├── .github ├── CODEOWNERS ├── ISSUE_TEMPLATE │ ├── bug_report.yml │ └── maintenance.yml ├── PULL_REQUEST_TEMPLATE.md ├── auto_assign.yml ├── boring-cyborg.yml ├── dependabot.yml ├── scripts │ ├── comment_on_large_pr.js │ ├── constants.js │ ├── download_pr_artifact.js │ ├── enforce_acknowledgment.js │ ├── label_missing_acknowledgement_section.js │ ├── label_missing_related_issue.js │ ├── label_pr_based_on_title.js │ ├── label_related_issue.js │ └── save_pr_details.js ├── semantic.yml └── workflows │ ├── auto_assign.yml │ ├── codeql-analysis.yml │ ├── label_pr_on_title.yml │ ├── on_label_added.yml │ ├── on_merged_pr.yml │ ├── on_opened_pr.yml │ ├── record_pr.yml │ ├── reusable_export_pr_details.yml │ ├── reusable_unit_tests.yml │ └── services_unit_tests.yml ├── .gitignore ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── docs ├── architecture.png └── workshop_logo.png ├── unicorn_contracts ├── .gitignore ├── Makefile ├── README.md ├── __init__.py ├── api.yaml ├── integration │ ├── ContractStatusChanged.json │ ├── event-schemas.yaml │ └── subscriber-policies.yaml ├── pyproject.toml ├── pytest.ini ├── ruff.toml ├── samconfig.toml ├── src │ └── contracts_service │ │ ├── __init__.py │ │ ├── contract_event_handler.py │ │ ├── enums.py │ │ └── exceptions.py ├── template.yaml ├── tests │ ├── __init__.py │ ├── integration │ │ ├── __init__.py │ │ ├── events │ │ │ ├── create_contract_invalid_payload_1.json │ │ │ ├── create_contract_valid_payload_1.json │ │ │ ├── update_existing_contract_invalid_payload_1.json │ │ │ ├── update_existing_contract_valid_payload_1.json │ │ │ ├── update_missing_contract_invalid_payload_1.json │ │ │ └── update_missing_contract_valid_payload_1.json │ │ ├── test_create_contract_apigw.py │ │ ├── test_update_contract_apigw.py │ │ └── transformations │ │ │ └── ddb_contract.jq │ ├── pipes │ │ ├── event_bridge_payloads │ │ │ ├── create_contract.json │ │ │ └── update_contract.json │ │ ├── pipes_payloads │ │ │ ├── create_contract.json │ │ │ └── update_contract.json │ │ └── streams_payloads │ │ │ ├── create_contract.json │ │ │ ├── delete_contract.json │ │ │ └── update_contract.json │ └── unit │ │ ├── __init__.py │ │ ├── conftest.py │ │ ├── event_generator.py │ │ ├── events │ │ ├── create_contract_invalid_1.json │ │ ├── create_contract_valid_1.json │ │ └── update_contract_valid_1.json │ │ ├── helper.py │ │ └── test_contract_event_handler.py └── uv.lock ├── unicorn_properties ├── .gitignore ├── Makefile ├── README.md ├── __init__.py ├── integration │ ├── PublicationEvaluationCompleted.json │ ├── event-schemas.yaml │ ├── subscriber-policies.yaml │ └── subscriptions.yaml ├── pyproject.toml ├── pytest.ini ├── ruff.toml ├── samconfig.toml ├── src │ ├── README.md │ ├── __init__.py │ ├── properties_service │ │ ├── __init__.py │ │ ├── contract_status_changed_event_handler.py │ │ ├── exceptions.py │ │ ├── properties_approval_sync_function.py │ │ └── wait_for_contract_approval_function.py │ └── schema │ │ ├── unicorn_contracts │ │ └── contractstatuschanged │ │ │ ├── AWSEvent.py │ │ │ ├── ContractStatusChanged.py │ │ │ ├── __init__.py │ │ │ └── marshaller.py │ │ └── unicorn_web │ │ └── publicationapprovalrequested │ │ ├── AWSEvent.py │ │ ├── PublicationApprovalRequested.py │ │ ├── __init__.py │ │ └── marshaller.py ├── state_machine │ └── property_approval.asl.yaml ├── template.yaml ├── tests │ ├── __init__.py │ └── unit │ │ ├── __init__.py │ │ ├── conftest.py │ │ ├── events │ │ ├── ddb_stream_events │ │ │ ├── contract_status_changed_draft.json │ │ │ ├── sfn_check_exists.json │ │ │ ├── sfn_wait_approval.json │ │ │ ├── status_approved_waiting_for_approval.json │ │ │ └── status_approved_with_no_workflow.json │ │ ├── eventbridge │ │ │ ├── contract_status_changed.json │ │ │ ├── contract_status_changed_event_contract_1_approved.json │ │ │ ├── contract_status_changed_event_contract_1_draft.json │ │ │ ├── contract_status_changed_event_contract_2_approved.json │ │ │ ├── contract_status_changed_event_contract_2_draft.json │ │ │ ├── publication_approval_requested_event.json │ │ │ ├── publication_approval_requested_event_all_good.json │ │ │ ├── publication_approval_requested_event_inappropriate_description.json │ │ │ ├── publication_approval_requested_event_inappropriate_images.json │ │ │ ├── publication_approval_requested_event_non_existing_contract.json │ │ │ ├── publication_approval_requested_event_pause_workflow.json │ │ │ ├── publication_evaluation_completed_event.json │ │ │ └── put_event_property_approval_requested.json │ │ └── lambda │ │ │ ├── content_integrity_validator_function_success.json │ │ │ ├── contract_status_checker.json │ │ │ └── wait_for_contract_approval_function.json │ │ ├── helper.py │ │ ├── test_contract_status_changed_event_handler.py │ │ ├── test_properties_approval_sync_function.py │ │ └── test_wait_for_contract_approval_function.py └── uv.lock ├── unicorn_shared ├── Makefile ├── uni-prop-images.yaml └── uni-prop-namespaces.yaml └── unicorn_web ├── .gitignore ├── Makefile ├── README.md ├── api.yaml ├── data ├── load_data.sh └── property_data.json ├── integration ├── PublicationApprovalRequested.json ├── event-schemas.yaml ├── subscriber-policies.yaml └── subscriptions.yaml ├── pyproject.toml ├── pytest.ini ├── ruff.toml ├── samconfig.toml ├── src ├── approvals_service │ ├── __init__.py │ ├── publication_approved_event_handler.py │ └── request_approval_function.py ├── schema │ └── unicorn_properties │ │ └── publicationevaluationcompleted │ │ ├── AWSEvent.py │ │ ├── PublicationEvaluationCompleted.py │ │ ├── __init__.py │ │ └── marshaller.py └── search_service │ ├── __init__.py │ └── property_search_function.py ├── template.yaml ├── tests ├── __init__.py ├── events │ └── eventbridge │ │ └── put_event_publication_evaluation_completed.json └── unit │ ├── __init__.py │ ├── conftest.py │ ├── event_generator.py │ ├── events │ ├── property_approved.json │ ├── request_already_approved.json │ ├── request_approval_bad_input.json │ ├── request_approval_event.json │ ├── request_invalid_property_id.json │ ├── request_non_existent_property.json │ ├── search_by_city.json │ ├── search_by_full_address.json │ ├── search_by_full_address_declined.json │ ├── search_by_full_address_new.json │ ├── search_by_full_address_not_found.json │ └── search_by_street_event.json │ ├── helper.py │ ├── test_publication_approved_event_handler.py │ ├── test_request_approval_function.py │ └── test_search_function.py └── uv.lock /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | # https://docs.github.com/en/repositories/managing-your-repositorys-settings-and-features/customizing-your-repository/about-code-owners 2 | 3 | * @aws-samples/aws-serverless-developer-experience-workshop-py 4 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.yml: -------------------------------------------------------------------------------- 1 | name: Bug report 2 | description: Report a reproducible bug to help us improve 3 | title: "Bug: TITLE" 4 | labels: ["bug", "triage"] 5 | body: 6 | - type: markdown 7 | attributes: 8 | value: | 9 | Thank you for submitting a bug report. Please add as much information as possible to help us reproduce, and remove any potential sensitive data. 10 | - type: textarea 11 | id: expected_behaviour 12 | attributes: 13 | label: Expected Behaviour 14 | description: Please share details on the behaviour you expected 15 | validations: 16 | required: true 17 | - type: textarea 18 | id: current_behaviour 19 | attributes: 20 | label: Current Behaviour 21 | description: Please share details on the current issue 22 | validations: 23 | required: true 24 | - type: textarea 25 | id: code_snippet 26 | attributes: 27 | label: Code snippet 28 | description: Please share a code snippet to help us reproduce the issue 29 | render: csharp 30 | validations: 31 | required: true 32 | - type: textarea 33 | id: solution 34 | attributes: 35 | label: Possible Solution 36 | description: If known, please suggest a potential resolution 37 | validations: 38 | required: false 39 | - type: textarea 40 | id: steps 41 | attributes: 42 | label: Steps to Reproduce 43 | description: Please share how we might be able to reproduce this issue 44 | validations: 45 | required: true 46 | - type: textarea 47 | id: logs 48 | attributes: 49 | label: Debugging logs 50 | description: If available, please share debugging logs 51 | render: csharp 52 | validations: 53 | required: false 54 | - type: markdown 55 | attributes: 56 | value: | 57 | -- 58 | **Disclaimer**: We value your time and bandwidth. As such, any pull requests created on non-triaged issues might not be successful. 59 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/maintenance.yml: -------------------------------------------------------------------------------- 1 | name: Maintenance 2 | description: Suggest an activity to help address tech debt, governance, and anything internal 3 | title: "Maintenance: TITLE" 4 | labels: ["internal", "triage"] 5 | body: 6 | - type: markdown 7 | attributes: 8 | value: | 9 | Thank you for taking the time to help us improve operational excellence. 10 | 11 | *Future readers*: Please react with 👍 and your use case to help us understand customer demand. 12 | - type: textarea 13 | id: activity 14 | attributes: 15 | label: Summary 16 | description: Please provide an overview in one or two paragraphs 17 | validations: 18 | required: true 19 | - type: textarea 20 | id: importance 21 | attributes: 22 | label: Why is this needed? 23 | description: Please help us understand the value so we can prioritize it accordingly 24 | validations: 25 | required: true 26 | - type: textarea 27 | id: suggestion 28 | attributes: 29 | label: Solution 30 | description: If available, please share what a good solution would look like 31 | validations: 32 | required: false 33 | - type: markdown 34 | attributes: 35 | value: | 36 | --- 37 | **Disclaimer**: We value your time and bandwidth. As such, any pull requests created on non-triaged issues might not be successful. 38 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | **Issue number:** 2 | 3 | ## Summary 4 | 5 | ### Changes 6 | 7 | > Please provide a summary of what's being changed 8 | 9 | ### User experience 10 | 11 | > Please share what the user experience looks like before and after this change 12 | 13 | ## Checklist 14 | 15 | Please leave checklist items unchecked if they do not apply to your change. 16 | 17 | * [ ] I have performed a self-review of this change 18 | * [ ] Changes have been tested 19 | * [ ] Changes are documented 20 | * [ ] PR title follows [conventional commit semantics](https://github.com/aws-samples/aws-serverless-developer-experience-workshop-python/blob/develop/.github/semantic.yml) 21 | 22 | ## Acknowledgment 23 | 24 | By submitting this pull request, I confirm that you can use, modify, copy, and redistribute this contribution, under the terms of your choice. 25 | 26 | **Disclaimer**: We value your time and bandwidth. As such, any pull requests created on non-triaged issues might not be successful. 27 | -------------------------------------------------------------------------------- /.github/auto_assign.yml: -------------------------------------------------------------------------------- 1 | # Set to true to add reviewers to pull requests 2 | addReviewers: true 3 | 4 | # Set to true to add assignees to pull requests 5 | addAssignees: true 6 | 7 | # A list of reviewers to be added to pull requests (GitHub user name) 8 | reviewers: 9 | - igorlg 10 | 11 | # A number of reviewers added to the pull request 12 | # Set 0 to add all the reviewers (default: 0) 13 | numberOfReviewers: 1 14 | 15 | # A list of assignees, overrides reviewers if set 16 | # assignees: 17 | # - sliedig 18 | 19 | # A number of assignees to add to the pull request 20 | # Set to 0 to add all of the assignees. 21 | # Uses numberOfReviewers if unset. 22 | numberOfAssignees: 0 23 | 24 | # A list of keywords to be skipped the process that add reviewers if pull requests include it 25 | # skipKeywords: 26 | # - wip 27 | -------------------------------------------------------------------------------- /.github/boring-cyborg.yml: -------------------------------------------------------------------------------- 1 | ##### Labeler ########################################################################################################## 2 | labelPRBasedOnFilePath: 3 | service/contracts: 4 | - Unicorn.Contracts 5 | service/properties: 6 | - Unicorn.Properties 7 | service/web: 8 | - Unicorn.Web 9 | 10 | internal: 11 | - .github/* 12 | - .github/**/* 13 | - .chglog/* 14 | - .flake8 15 | - .gitignore 16 | - .pre-commit-config.yaml 17 | - Makefile 18 | - CONTRIBUTING.md 19 | - CODE_OF_CONDUCT.md 20 | - LICENSE 21 | 22 | ##### Greetings ######################################################################################################## 23 | firstPRWelcomeComment: > 24 | Thanks a lot for your first contribution! Please check out our contributing guidelines and don't hesitate to ask whatever you need. 25 | 26 | # Comment to be posted to congratulate user on their first merged PR 27 | firstPRMergeComment: > 28 | Awesome work, congrats on your first merged pull request and thank you for helping improve everyone's experience! 29 | 30 | # Comment to be posted to on first time issues 31 | firstIssueWelcomeComment: > 32 | Thanks for opening your first issue here! We'll come back to you as soon as we can. -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # To get started with Dependabot version updates, you'll need to specify which 2 | # package ecosystems to update and where the package manifests are located. 3 | # Please see the documentation for all configuration options: 4 | # https://docs.github.com/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file 5 | 6 | version: 2 7 | updates: 8 | - package-ecosystem: "pip" # See documentation for possible values 9 | directories: 10 | - "unicorn_contracts" # Location of package manifests 11 | - "unicorn_properties" 12 | - "unicorn_web" 13 | schedule: 14 | interval: "weekly" 15 | -------------------------------------------------------------------------------- /.github/scripts/comment_on_large_pr.js: -------------------------------------------------------------------------------- 1 | const { 2 | PR_NUMBER, 3 | PR_ACTION, 4 | PR_AUTHOR, 5 | IGNORE_AUTHORS, 6 | } = require("./constants") 7 | 8 | 9 | /** 10 | * Notify PR author to split XXL PR in smaller chunks 11 | * 12 | * @param {object} core - core functions instance from @actions/core 13 | * @param {object} gh_client - Pre-authenticated REST client (Octokit) 14 | * @param {string} owner - GitHub Organization 15 | * @param {string} repository - GitHub repository 16 | */ 17 | const notifyAuthor = async ({ 18 | core, 19 | gh_client, 20 | owner, 21 | repository, 22 | }) => { 23 | core.info(`Commenting on PR ${PR_NUMBER}`) 24 | 25 | let msg = `### ⚠️Large PR detected⚠️ 26 | 27 | Please consider breaking into smaller PRs to avoid significant review delays. Ignore if this PR has naturally grown to this size after reviews. 28 | `; 29 | 30 | try { 31 | await gh_client.rest.issues.createComment({ 32 | owner: owner, 33 | repo: repository, 34 | body: msg, 35 | issue_number: PR_NUMBER, 36 | }); 37 | } catch (error) { 38 | core.setFailed("Failed to notify PR author to split large PR"); 39 | console.error(err); 40 | } 41 | } 42 | 43 | module.exports = async ({github, context, core}) => { 44 | if (IGNORE_AUTHORS.includes(PR_AUTHOR)) { 45 | return core.notice("Author in IGNORE_AUTHORS list; skipping...") 46 | } 47 | 48 | if (PR_ACTION != "labeled") { 49 | return core.notice("Only run on PRs labeling actions; skipping") 50 | } 51 | 52 | 53 | /** @type {string[]} */ 54 | const { data: labels } = await github.rest.issues.listLabelsOnIssue({ 55 | owner: context.repo.owner, 56 | repo: context.repo.repo, 57 | issue_number: PR_NUMBER, 58 | }) 59 | 60 | // Schema: https://docs.github.com/en/rest/issues/labels#list-labels-for-an-issue 61 | for (const label of labels) { 62 | core.info(`Label: ${label}`) 63 | if (label.name == "size/XXL") { 64 | await notifyAuthor({ 65 | core: core, 66 | gh_client: github, 67 | owner: context.repo.owner, 68 | repository: context.repo.repo, 69 | }) 70 | break; 71 | } 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /.github/scripts/constants.js: -------------------------------------------------------------------------------- 1 | module.exports = Object.freeze({ 2 | /** @type {string} */ 3 | // Values: https://docs.github.com/en/actions/using-workflows/events-that-trigger-workflows#pull_request 4 | "PR_ACTION": process.env.PR_ACTION?.replace(/"/g, '') || "", 5 | 6 | /** @type {string} */ 7 | "PR_AUTHOR": process.env.PR_AUTHOR?.replace(/"/g, '') || "", 8 | 9 | /** @type {string} */ 10 | "PR_BODY": process.env.PR_BODY || "", 11 | 12 | /** @type {string} */ 13 | "PR_TITLE": process.env.PR_TITLE || "", 14 | 15 | /** @type {number} */ 16 | "PR_NUMBER": process.env.PR_NUMBER || 0, 17 | 18 | /** @type {string} */ 19 | "PR_IS_MERGED": process.env.PR_IS_MERGED || "false", 20 | 21 | /** @type {string} */ 22 | "LABEL_BLOCK": "do-not-merge", 23 | 24 | /** @type {string} */ 25 | "LABEL_BLOCK_REASON": "need-issue", 26 | 27 | /** @type {string} */ 28 | "LABEL_BLOCK_MISSING_LICENSE_AGREEMENT": "need-license-agreement-acknowledge", 29 | 30 | /** @type {string} */ 31 | "LABEL_PENDING_RELEASE": "pending-release", 32 | 33 | /** @type {string[]} */ 34 | "IGNORE_AUTHORS": ["dependabot[bot]", "markdownify[bot]"], 35 | 36 | }); 37 | -------------------------------------------------------------------------------- /.github/scripts/download_pr_artifact.js: -------------------------------------------------------------------------------- 1 | module.exports = async ({github, context, core}) => { 2 | const fs = require('fs'); 3 | 4 | const workflowRunId = process.env.WORKFLOW_ID; 5 | core.info(`Listing artifacts for workflow run ${workflowRunId}`); 6 | 7 | const artifacts = await github.rest.actions.listWorkflowRunArtifacts({ 8 | owner: context.repo.owner, 9 | repo: context.repo.repo, 10 | run_id: workflowRunId, 11 | }); 12 | 13 | const matchArtifact = artifacts.data.artifacts.filter(artifact => artifact.name == "pr")[0]; 14 | 15 | core.info(`Downloading artifacts for workflow run ${workflowRunId}`); 16 | const artifact = await github.rest.actions.downloadArtifact({ 17 | owner: context.repo.owner, 18 | repo: context.repo.repo, 19 | artifact_id: matchArtifact.id, 20 | archive_format: 'zip', 21 | }); 22 | 23 | core.info("Saving artifact found", artifact); 24 | 25 | fs.writeFileSync('pr.zip', Buffer.from(artifact.data)); 26 | } 27 | -------------------------------------------------------------------------------- /.github/scripts/enforce_acknowledgment.js: -------------------------------------------------------------------------------- 1 | const { 2 | PR_ACTION, 3 | PR_AUTHOR, 4 | PR_BODY, 5 | PR_NUMBER, 6 | IGNORE_AUTHORS, 7 | LABEL_BLOCK, 8 | LABEL_BLOCK_REASON 9 | } = require("./constants") 10 | 11 | module.exports = async ({github, context, core}) => { 12 | if (IGNORE_AUTHORS.includes(PR_AUTHOR)) { 13 | return core.notice("Author in IGNORE_AUTHORS list; skipping...") 14 | } 15 | 16 | if (PR_ACTION != "opened") { 17 | return core.notice("Only newly open PRs are labelled to avoid spam; skipping") 18 | } 19 | 20 | const RELATED_ISSUE_REGEX = /Issue number:[^\d\r\n]+(?\d+)/; 21 | const isMatch = RELATED_ISSUE_REGEX.exec(PR_BODY); 22 | if (isMatch == null) { 23 | core.info(`No related issue found, maybe the author didn't use the template but there is one.`) 24 | 25 | let msg = "No related issues found. Please ensure there is an open issue related to this change to avoid significant delays or closure."; 26 | await github.rest.issues.createComment({ 27 | owner: context.repo.owner, 28 | repo: context.repo.repo, 29 | body: msg, 30 | issue_number: PR_NUMBER, 31 | }); 32 | 33 | return await github.rest.issues.addLabels({ 34 | issue_number: PR_NUMBER, 35 | owner: context.repo.owner, 36 | repo: context.repo.repo, 37 | labels: [LABEL_BLOCK, LABEL_BLOCK_REASON] 38 | }) 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /.github/scripts/label_missing_acknowledgement_section.js: -------------------------------------------------------------------------------- 1 | const { 2 | PR_ACTION, 3 | PR_AUTHOR, 4 | PR_BODY, 5 | PR_NUMBER, 6 | IGNORE_AUTHORS, 7 | LABEL_BLOCK, 8 | LABEL_BLOCK_MISSING_LICENSE_AGREEMENT 9 | } = require("./constants") 10 | 11 | module.exports = async ({github, context, core}) => { 12 | if (IGNORE_AUTHORS.includes(PR_AUTHOR)) { 13 | return core.notice("Author in IGNORE_AUTHORS list; skipping...") 14 | } 15 | 16 | if (PR_ACTION != "opened") { 17 | return core.notice("Only newly open PRs are labelled to avoid spam; skipping") 18 | } 19 | 20 | const RELATED_ACK_SECTION_REGEX = /By submitting this pull request, I confirm that you can use, modify, copy, and redistribute this contribution, under the terms of your choice./; 21 | 22 | const isMatch = RELATED_ACK_SECTION_REGEX.exec(PR_BODY); 23 | if (isMatch == null) { 24 | core.info(`No acknowledgement section found, maybe the author didn't use the template but there is one.`) 25 | 26 | let msg = "No acknowledgement section found. Please make sure you used the template to open a PR and didn't remove the acknowledgment section. Check the template here: https://github.com/awslabs/aws-lambda-powertools-python/blob/develop/.github/PULL_REQUEST_TEMPLATE.md#acknowledgment"; 27 | await github.rest.issues.createComment({ 28 | owner: context.repo.owner, 29 | repo: context.repo.repo, 30 | body: msg, 31 | issue_number: PR_NUMBER, 32 | }); 33 | 34 | return await github.rest.issues.addLabels({ 35 | issue_number: PR_NUMBER, 36 | owner: context.repo.owner, 37 | repo: context.repo.repo, 38 | labels: [LABEL_BLOCK, LABEL_BLOCK_MISSING_LICENSE_AGREEMENT] 39 | }) 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /.github/scripts/label_missing_related_issue.js: -------------------------------------------------------------------------------- 1 | const { 2 | PR_ACTION, 3 | PR_AUTHOR, 4 | PR_BODY, 5 | PR_NUMBER, 6 | IGNORE_AUTHORS, 7 | LABEL_BLOCK, 8 | LABEL_BLOCK_REASON 9 | } = require("./constants") 10 | 11 | module.exports = async ({github, context, core}) => { 12 | if (IGNORE_AUTHORS.includes(PR_AUTHOR)) { 13 | return core.notice("Author in IGNORE_AUTHORS list; skipping...") 14 | } 15 | 16 | if (PR_ACTION != "opened") { 17 | return core.notice("Only newly open PRs are labelled to avoid spam; skipping") 18 | } 19 | 20 | const RELATED_ISSUE_REGEX = /Issue number:[^\d\r\n]+(?\d+)/; 21 | const isMatch = RELATED_ISSUE_REGEX.exec(PR_BODY); 22 | if (isMatch == null) { 23 | core.info(`No related issue found, maybe the author didn't use the template but there is one.`) 24 | 25 | let msg = "No related issues found. Please ensure there is an open issue related to this change to avoid significant delays or closure."; 26 | await github.rest.issues.createComment({ 27 | owner: context.repo.owner, 28 | repo: context.repo.repo, 29 | body: msg, 30 | issue_number: PR_NUMBER, 31 | }); 32 | 33 | return await github.rest.issues.addLabels({ 34 | issue_number: PR_NUMBER, 35 | owner: context.repo.owner, 36 | repo: context.repo.repo, 37 | labels: [LABEL_BLOCK, LABEL_BLOCK_REASON] 38 | }) 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /.github/scripts/label_pr_based_on_title.js: -------------------------------------------------------------------------------- 1 | const { PR_NUMBER, PR_TITLE } = require("./constants") 2 | 3 | module.exports = async ({github, context, core}) => { 4 | const FEAT_REGEX = /feat(\((.+)\))?(:.+)/ 5 | const BUG_REGEX = /(fix|bug)(\((.+)\))?(:.+)/ 6 | const DOCS_REGEX = /(docs|doc)(\((.+)\))?(:.+)/ 7 | const CHORE_REGEX = /(chore)(\((.+)\))?(:.+)/ 8 | const DEPRECATED_REGEX = /(deprecated)(\((.+)\))?(:.+)/ 9 | const REFACTOR_REGEX = /(refactor)(\((.+)\))?(:.+)/ 10 | 11 | const labels = { 12 | "feature": FEAT_REGEX, 13 | "bug": BUG_REGEX, 14 | "documentation": DOCS_REGEX, 15 | "internal": CHORE_REGEX, 16 | "enhancement": REFACTOR_REGEX, 17 | "deprecated": DEPRECATED_REGEX, 18 | } 19 | 20 | // Maintenance: We should keep track of modified PRs in case their titles change 21 | let miss = 0; 22 | try { 23 | for (const label in labels) { 24 | const matcher = new RegExp(labels[label]) 25 | const matches = matcher.exec(PR_TITLE) 26 | if (matches != null) { 27 | core.info(`Auto-labeling PR ${PR_NUMBER} with ${label}`) 28 | 29 | await github.rest.issues.addLabels({ 30 | issue_number: PR_NUMBER, 31 | owner: context.repo.owner, 32 | repo: context.repo.repo, 33 | labels: [label] 34 | }) 35 | 36 | return; 37 | } else { 38 | core.debug(`'${PR_TITLE}' didn't match '${label}' semantic.`) 39 | miss += 1 40 | } 41 | } 42 | } finally { 43 | if (miss == Object.keys(labels).length) { 44 | core.notice(`PR ${PR_NUMBER} title '${PR_TITLE}' doesn't follow semantic titles; skipping...`) 45 | } 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /.github/scripts/label_related_issue.js: -------------------------------------------------------------------------------- 1 | const { 2 | PR_AUTHOR, 3 | PR_BODY, 4 | PR_NUMBER, 5 | IGNORE_AUTHORS, 6 | LABEL_PENDING_RELEASE, 7 | HANDLE_MAINTAINERS_TEAM, 8 | PR_IS_MERGED, 9 | } = require("./constants") 10 | 11 | module.exports = async ({github, context, core}) => { 12 | if (IGNORE_AUTHORS.includes(PR_AUTHOR)) { 13 | return core.notice("Author in IGNORE_AUTHORS list; skipping...") 14 | } 15 | 16 | if (PR_IS_MERGED == "false") { 17 | return core.notice("Only merged PRs to avoid spam; skipping") 18 | } 19 | 20 | const RELATED_ISSUE_REGEX = /Issue number:[^\d\r\n]+(?\d+)/; 21 | 22 | const isMatch = RELATED_ISSUE_REGEX.exec(PR_BODY); 23 | 24 | try { 25 | if (!isMatch) { 26 | core.setFailed(`Unable to find related issue for PR number ${PR_NUMBER}.\n\n Body details: ${PR_BODY}`); 27 | return await github.rest.issues.createComment({ 28 | owner: context.repo.owner, 29 | repo: context.repo.repo, 30 | body: `${HANDLE_MAINTAINERS_TEAM} No related issues found. Please ensure '${LABEL_PENDING_RELEASE}' label is applied before releasing.`, 31 | issue_number: PR_NUMBER, 32 | }); 33 | } 34 | } catch (error) { 35 | core.setFailed(`Unable to create comment on PR number ${PR_NUMBER}.\n\n Error details: ${error}`); 36 | throw new Error(error); 37 | } 38 | 39 | const { groups: {issue} } = isMatch 40 | 41 | try { 42 | core.info(`Auto-labeling related issue ${issue} for release`) 43 | return await github.rest.issues.addLabels({ 44 | issue_number: issue, 45 | owner: context.repo.owner, 46 | repo: context.repo.repo, 47 | labels: [LABEL_PENDING_RELEASE] 48 | }) 49 | } catch (error) { 50 | core.setFailed(`Is this issue number (${issue}) valid? Perhaps a discussion?`); 51 | throw new Error(error); 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /.github/scripts/save_pr_details.js: -------------------------------------------------------------------------------- 1 | module.exports = async ({context, core}) => { 2 | const fs = require('fs'); 3 | const filename = "pr.txt"; 4 | 5 | try { 6 | fs.writeFileSync(`./${filename}`, JSON.stringify(context.payload)); 7 | 8 | return `PR successfully saved ${filename}` 9 | } catch (err) { 10 | core.setFailed("Failed to save PR details"); 11 | console.error(err); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /.github/semantic.yml: -------------------------------------------------------------------------------- 1 | # conventional commit types: https://github.com/commitizen/conventional-commit-types/blob/v3.0.0/index.json 2 | types: 3 | - feat 4 | - fix 5 | - docs 6 | - style 7 | - refactor 8 | - perf 9 | - test 10 | - build 11 | - ci 12 | - chore 13 | - revert 14 | - improv 15 | 16 | # Always validate the PR title 17 | # and ignore the commits to lower the entry bar for contribution 18 | # while titles make up the Release notes to ease maintenance overhead 19 | titleOnly: true 20 | -------------------------------------------------------------------------------- /.github/workflows/auto_assign.yml: -------------------------------------------------------------------------------- 1 | name: 'Auto Assign' 2 | on: 3 | pull_request: 4 | types: [opened, ready_for_review] 5 | 6 | jobs: 7 | add-reviews: 8 | runs-on: ubuntu-latest 9 | steps: 10 | - uses: kentaro-m/auto-assign-action@v1.2.5 -------------------------------------------------------------------------------- /.github/workflows/codeql-analysis.yml: -------------------------------------------------------------------------------- 1 | name: "CodeQL" 2 | 3 | on: 4 | push: 5 | branches: [ "develop", main ] 6 | pull_request: 7 | # The branches below must be a subset of the branches above 8 | branches: [ "develop" ] 9 | schedule: 10 | - cron: '42 8 * * 0' 11 | 12 | jobs: 13 | analyze: 14 | name: Analyze 15 | runs-on: ubuntu-latest 16 | permissions: 17 | actions: read 18 | contents: read 19 | security-events: write 20 | 21 | strategy: 22 | fail-fast: false 23 | matrix: 24 | language: [ 'python' ] 25 | 26 | steps: 27 | - name: Checkout repository 28 | uses: actions/checkout@v3 29 | 30 | # Initializes the CodeQL tools for scanning. 31 | - name: Initialize CodeQL 32 | uses: github/codeql-action/init@2ca79b6fa8d3ec278944088b4aa5f46912db5d63 #v2 33 | with: 34 | languages: ${{ matrix.language }} 35 | 36 | # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). 37 | # If this step fails, then you should remove it and run the build manually (see below) 38 | # - name: Autobuild 39 | # uses: github/codeql-action/autobuild@2ca79b6fa8d3ec278944088b4aa5f46912db5d63 #v2 40 | 41 | # ℹ️ Command-line programs to run using the OS shell. 42 | # 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun 43 | 44 | # If the Autobuild fails above, remove it and uncomment the following three lines. 45 | # modify them (or add more) to build your code if your project, please refer to the EXAMPLE below for guidance. 46 | 47 | # - run: | 48 | # echo "Run, Build Application using script" 49 | # ./location_of_script_within_repo/buildscript.sh 50 | 51 | - name: Perform CodeQL Analysis 52 | uses: github/codeql-action/analyze@2ca79b6fa8d3ec278944088b4aa5f46912db5d63 #v2 53 | -------------------------------------------------------------------------------- /.github/workflows/label_pr_on_title.yml: -------------------------------------------------------------------------------- 1 | name: Label PR based on title 2 | 3 | on: 4 | workflow_run: 5 | workflows: ["Record PR details"] 6 | types: 7 | - completed 8 | 9 | jobs: 10 | get_pr_details: 11 | # Guardrails to only ever run if PR recording workflow was indeed 12 | # run in a PR event and ran successfully 13 | if: ${{ github.event.workflow_run.conclusion == 'success' }} 14 | uses: ./.github/workflows/reusable_export_pr_details.yml 15 | with: 16 | record_pr_workflow_id: ${{ github.event.workflow_run.id }} 17 | workflow_origin: ${{ github.event.repository.full_name }} 18 | secrets: 19 | token: ${{ secrets.GITHUB_TOKEN }} 20 | label_pr: 21 | needs: get_pr_details 22 | runs-on: ubuntu-latest 23 | steps: 24 | - name: Checkout repository 25 | uses: actions/checkout@v3 26 | - name: "Label PR based on title" 27 | uses: actions/github-script@v6 28 | env: 29 | PR_NUMBER: ${{ needs.get_pr_details.outputs.prNumber }} 30 | PR_TITLE: ${{ needs.get_pr_details.outputs.prTitle }} 31 | with: 32 | github-token: ${{ secrets.GITHUB_TOKEN }} 33 | # This safely runs in our base repo, not on fork 34 | # thus allowing us to provide a write access token to label based on PR title 35 | # and label PR based on semantic title accordingly 36 | script: | 37 | const script = require('.github/scripts/label_pr_based_on_title.js') 38 | await script({github, context, core}) 39 | -------------------------------------------------------------------------------- /.github/workflows/on_label_added.yml: -------------------------------------------------------------------------------- 1 | name: On Label added 2 | 3 | on: 4 | workflow_run: 5 | workflows: ["Record PR details"] 6 | types: 7 | - completed 8 | 9 | jobs: 10 | get_pr_details: 11 | if: ${{ github.event.workflow_run.conclusion == 'success' }} 12 | uses: ./.github/workflows/reusable_export_pr_details.yml 13 | with: 14 | record_pr_workflow_id: ${{ github.event.workflow_run.id }} 15 | workflow_origin: ${{ github.event.repository.full_name }} 16 | secrets: 17 | token: ${{ secrets.GITHUB_TOKEN }} 18 | 19 | split-large-pr: 20 | needs: get_pr_details 21 | runs-on: ubuntu-latest 22 | permissions: 23 | issues: write 24 | pull-requests: write 25 | steps: 26 | - uses: actions/checkout@v3 27 | # Maintenance: Persist state per PR as an artifact to avoid spam on label add 28 | - name: "Suggest split large Pull Request" 29 | uses: actions/github-script@v6 30 | env: 31 | PR_NUMBER: ${{ needs.get_pr_details.outputs.prNumber }} 32 | PR_ACTION: ${{ needs.get_pr_details.outputs.prAction }} 33 | PR_AUTHOR: ${{ needs.get_pr_details.outputs.prAuthor }} 34 | with: 35 | github-token: ${{ secrets.GITHUB_TOKEN }} 36 | script: | 37 | const script = require('.github/scripts/comment_on_large_pr.js'); 38 | await script({github, context, core}); 39 | -------------------------------------------------------------------------------- /.github/workflows/on_merged_pr.yml: -------------------------------------------------------------------------------- 1 | name: On PR merge 2 | 3 | on: 4 | workflow_run: 5 | workflows: ["Record PR details"] 6 | types: 7 | - completed 8 | 9 | jobs: 10 | get_pr_details: 11 | if: github.event.workflow_run.event == 'pull_request' && github.event.workflow_run.conclusion == 'success' 12 | uses: ./.github/workflows/reusable_export_pr_details.yml 13 | with: 14 | record_pr_workflow_id: ${{ github.event.workflow_run.id }} 15 | workflow_origin: ${{ github.event.repository.full_name }} 16 | secrets: 17 | token: ${{ secrets.GITHUB_TOKEN }} 18 | release_label_on_merge: 19 | needs: get_pr_details 20 | runs-on: ubuntu-latest 21 | if: needs.get_pr_details.outputs.prIsMerged == 'true' 22 | steps: 23 | - uses: actions/checkout@v3 24 | - name: "Label PR related issue for release" 25 | uses: actions/github-script@v6 26 | env: 27 | PR_NUMBER: ${{ needs.get_pr_details.outputs.prNumber }} 28 | PR_BODY: ${{ needs.get_pr_details.outputs.prBody }} 29 | PR_IS_MERGED: ${{ needs.get_pr_details.outputs.prIsMerged }} 30 | PR_AUTHOR: ${{ needs.get_pr_details.outputs.prAuthor }} 31 | with: 32 | github-token: ${{ secrets.GITHUB_TOKEN }} 33 | script: | 34 | const script = require('.github/scripts/label_related_issue.js') 35 | await script({github, context, core}) 36 | -------------------------------------------------------------------------------- /.github/workflows/on_opened_pr.yml: -------------------------------------------------------------------------------- 1 | name: On new PR 2 | 3 | on: 4 | workflow_run: 5 | workflows: ["Record PR details"] 6 | types: 7 | - completed 8 | 9 | jobs: 10 | get_pr_details: 11 | if: ${{ github.event.workflow_run.conclusion == 'success' }} 12 | uses: ./.github/workflows/reusable_export_pr_details.yml 13 | with: 14 | record_pr_workflow_id: ${{ github.event.workflow_run.id }} 15 | workflow_origin: ${{ github.event.repository.full_name }} 16 | secrets: 17 | token: ${{ secrets.GITHUB_TOKEN }} 18 | check_related_issue: 19 | needs: get_pr_details 20 | runs-on: ubuntu-latest 21 | steps: 22 | - uses: actions/checkout@v3 23 | - name: "Ensure related issue is present" 24 | uses: actions/github-script@v6 25 | env: 26 | PR_BODY: ${{ needs.get_pr_details.outputs.prBody }} 27 | PR_NUMBER: ${{ needs.get_pr_details.outputs.prNumber }} 28 | PR_ACTION: ${{ needs.get_pr_details.outputs.prAction }} 29 | PR_AUTHOR: ${{ needs.get_pr_details.outputs.prAuthor }} 30 | with: 31 | github-token: ${{ secrets.GITHUB_TOKEN }} 32 | script: | 33 | const script = require('.github/scripts/label_missing_related_issue.js') 34 | await script({github, context, core}) 35 | check_acknowledge_section: 36 | needs: get_pr_details 37 | runs-on: ubuntu-latest 38 | steps: 39 | - uses: actions/checkout@v3 40 | - name: "Ensure acknowledgement section is present" 41 | uses: actions/github-script@v6 42 | env: 43 | PR_BODY: ${{ needs.get_pr_details.outputs.prBody }} 44 | PR_NUMBER: ${{ needs.get_pr_details.outputs.prNumber }} 45 | PR_ACTION: ${{ needs.get_pr_details.outputs.prAction }} 46 | PR_AUTHOR: ${{ needs.get_pr_details.outputs.prAuthor }} 47 | with: 48 | github-token: ${{ secrets.GITHUB_TOKEN }} 49 | script: | 50 | const script = require('.github/scripts/label_missing_acknowledgement_section.js') 51 | await script({github, context, core}) 52 | -------------------------------------------------------------------------------- /.github/workflows/record_pr.yml: -------------------------------------------------------------------------------- 1 | name: Record PR details 2 | 3 | on: 4 | pull_request: 5 | types: [opened, edited, closed] 6 | 7 | jobs: 8 | record_pr: 9 | runs-on: ubuntu-latest 10 | 11 | steps: 12 | - uses: actions/checkout@v3 13 | - name: "Extract PR details" 14 | uses: actions/github-script@v6 15 | with: 16 | script: | 17 | const script = require('.github/scripts/save_pr_details.js') 18 | await script({github, context, core}) 19 | - uses: actions/upload-artifact@v3 20 | with: 21 | name: pr 22 | path: pr.txt 23 | -------------------------------------------------------------------------------- /.github/workflows/reusable_unit_tests.yml: -------------------------------------------------------------------------------- 1 | name: Run Unit Tests for Python services 2 | 3 | on: 4 | workflow_call: 5 | inputs: 6 | service_directory: 7 | type: string 8 | required: true 9 | 10 | jobs: 11 | unit_tests: 12 | runs-on: ubuntu-latest 13 | 14 | steps: 15 | - uses: actions/checkout@v3 16 | 17 | - uses: actions/setup-python@v4 18 | with: { python-version: 3.12 } 19 | 20 | - name: Install uv 21 | uses: astral-sh/setup-uv@v5 22 | with: 23 | version: 0.7.8 24 | 25 | - name: Initialize and install dependencies 26 | run: make ci_init 27 | working-directory: ./${{ inputs.service_directory }} 28 | 29 | - name: Run Unit tests 30 | run: make unit-test 31 | working-directory: ./${{ inputs.service_directory }} 32 | -------------------------------------------------------------------------------- /.github/workflows/services_unit_tests.yml: -------------------------------------------------------------------------------- 1 | on: 2 | push: 3 | branches: [develop, main] 4 | paths: 5 | - 'unicorn_contracts/**' 6 | - 'unicorn_properties/**' 7 | - 'unicorn_web/**' 8 | pull_request: 9 | branches: [develop, main] 10 | paths: 11 | - 'unicorn_contracts/**' 12 | - 'unicorn_properties/**' 13 | - 'unicorn_web/**' 14 | 15 | jobs: 16 | unicorn_contracts: 17 | uses: ./.github/workflows/reusable_unit_tests.yml 18 | with: 19 | service_directory: unicorn_contracts 20 | 21 | unicorn_properties: 22 | uses: ./.github/workflows/reusable_unit_tests.yml 23 | with: 24 | service_directory: unicorn_properties 25 | 26 | unicorn_web: 27 | uses: ./.github/workflows/reusable_unit_tests.yml 28 | with: 29 | service_directory: unicorn_web 30 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | .idea/* 3 | **/.aws-sam/ 4 | .vscode/* 5 | .vscode/settings.json 6 | **/cdk.out/ -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | ## Code of Conduct 2 | This project has adopted the [Amazon Open Source Code of Conduct](https://aws.github.io/code-of-conduct). 3 | For more information see the [Code of Conduct FAQ](https://aws.github.io/code-of-conduct-faq) or contact 4 | opensource-codeofconduct@amazon.com with any additional questions or comments. 5 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing Guidelines 2 | 3 | Thank you for your interest in contributing to our project. Whether it's a bug report, new feature, correction, or additional 4 | documentation, we greatly value feedback and contributions from our community. 5 | 6 | Please read through this document before submitting any issues or pull requests to ensure we have all the necessary 7 | information to effectively respond to your bug report or contribution. 8 | 9 | 10 | ## Reporting Bugs/Feature Requests 11 | 12 | We welcome you to use the GitHub issue tracker to report bugs or suggest features. 13 | 14 | When filing an issue, please check existing open, or recently closed, issues to make sure somebody else hasn't already 15 | reported the issue. Please try to include as much information as you can. Details like these are incredibly useful: 16 | 17 | * A reproducible test case or series of steps 18 | * The version of our code being used 19 | * Any modifications you've made relevant to the bug 20 | * Anything unusual about your environment or deployment 21 | 22 | 23 | ## Contributing via Pull Requests 24 | Contributions via pull requests are much appreciated. Before sending us a pull request, please ensure that: 25 | 26 | 1. You are working against the latest source on the *main* branch. 27 | 2. You check existing open, and recently merged, pull requests to make sure someone else hasn't addressed the problem already. 28 | 3. You open an issue to discuss any significant work - we would hate for your time to be wasted. 29 | 30 | To send us a pull request, please: 31 | 32 | 1. Fork the repository. 33 | 2. Modify the source; please focus on the specific change you are contributing. If you also reformat all the code, it will be hard for us to focus on your change. 34 | 3. Ensure local tests pass. 35 | 4. Commit to your fork using clear commit messages. 36 | 5. Send us a pull request, answering any default questions in the pull request interface. 37 | 6. Pay attention to any automated CI failures reported in the pull request, and stay involved in the conversation. 38 | 39 | GitHub provides additional document on [forking a repository](https://help.github.com/articles/fork-a-repo/) and 40 | [creating a pull request](https://help.github.com/articles/creating-a-pull-request/). 41 | 42 | 43 | ## Finding contributions to work on 44 | Looking at the existing issues is a great way to find something to contribute on. As our projects, by default, use the default GitHub issue labels (enhancement/bug/duplicate/help wanted/invalid/question/wontfix), looking at any 'help wanted' issues is a great place to start. 45 | 46 | 47 | ## Code of Conduct 48 | This project has adopted the [Amazon Open Source Code of Conduct](https://aws.github.io/code-of-conduct). 49 | For more information see the [Code of Conduct FAQ](https://aws.github.io/code-of-conduct-faq) or contact 50 | opensource-codeofconduct@amazon.com with any additional questions or comments. 51 | 52 | 53 | ## Security issue notifications 54 | If you discover a potential security issue in this project we ask that you notify AWS/Amazon Security via our [vulnerability reporting page](http://aws.amazon.com/security/vulnerability-reporting/). Please do **not** create a public github issue. 55 | 56 | 57 | ## Licensing 58 | 59 | See the [LICENSE](LICENSE) file for our project's licensing. We will ask you to confirm the licensing of your contribution. 60 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of 4 | this software and associated documentation files (the "Software"), to deal in 5 | the Software without restriction, including without limitation the rights to 6 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 7 | the Software, and to permit persons to whom the Software is furnished to do so. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 10 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 11 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 12 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 13 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 14 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 15 | 16 | -------------------------------------------------------------------------------- /docs/architecture.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/aws-serverless-developer-experience-workshop-python/c3e988cd9a2af13b672c1c79d122568333b4e48c/docs/architecture.png -------------------------------------------------------------------------------- /docs/workshop_logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/aws-serverless-developer-experience-workshop-python/c3e988cd9a2af13b672c1c79d122568333b4e48c/docs/workshop_logo.png -------------------------------------------------------------------------------- /unicorn_contracts/.gitignore: -------------------------------------------------------------------------------- 1 | ### Linux ### 2 | **/*~ 3 | **/.fuse_hidden* 4 | **/.directory 5 | **/.Trash-* 6 | **/.nfs* 7 | 8 | ### OSX ### 9 | **/*.DS_Store 10 | **/.AppleDouble 11 | **/.LSOverride 12 | **/.DocumentRevisions-V100 13 | **/.fseventsd 14 | **/.Spotlight-V100 15 | **/.TemporaryItems 16 | **/.Trashes 17 | **/.VolumeIcon.icns 18 | **/.com.apple.timemachine.donotpresent 19 | 20 | ### JetBrains IDEs ### 21 | **/*.iws 22 | **/.idea/ 23 | **/.idea_modules/ 24 | 25 | ### Python ### 26 | **/__pycache__/ 27 | **/*.py[cod] 28 | **/*$py.class 29 | **/.Python 30 | **/build/ 31 | **/develop-eggs/ 32 | **/dist/ 33 | **/downloads/ 34 | **/eggs/ 35 | **/.eggs/ 36 | **/parts/ 37 | **/sdist/ 38 | **/wheels/ 39 | **/*.egg 40 | **/*.egg-info/ 41 | **/.installed.cfg 42 | **/pip-log.txt 43 | **/pip-delete-this-directory.txt 44 | 45 | ### Unit test / coverage reports ### 46 | **/.cache 47 | **/.coverage 48 | **/.hypothesis/ 49 | **/.pytest_cache/ 50 | **/.tox/ 51 | **/*.cover 52 | **/coverage.xml 53 | **/htmlcov/ 54 | **/nosetests.xml 55 | 56 | ### pyenv ### 57 | **/.python-version 58 | 59 | ### Environments ### 60 | **/.env 61 | **/.venv 62 | **/env/ 63 | **/venv/ 64 | **/ENV/ 65 | **/env.bak/ 66 | **/venv.bak/ 67 | 68 | ### VisualStudioCode ### 69 | **/.vscode/ 70 | **/.history 71 | 72 | ### Windows ### 73 | # Windows thumbnail cache files 74 | **/Thumbs.db 75 | **/ehthumbs.db 76 | **/ehthumbs_vista.db 77 | **/Desktop.ini 78 | **/$RECYCLE.BIN/ 79 | **/*.lnk 80 | 81 | ### requirements.txt ### 82 | # We ignore Python's requirements.txt as we use Poetry instead 83 | **/requirements.txt 84 | **/.aws-sam 85 | -------------------------------------------------------------------------------- /unicorn_contracts/Makefile: -------------------------------------------------------------------------------- 1 | #### Global Variables 2 | stackName := $(shell yq -r '.default.global.parameters.stack_name' samconfig.toml) 3 | 4 | 5 | #### Test Variables 6 | apiUrl = $(call cf_output,$(stackName),ApiUrl) 7 | ddbPropertyId = $(call get_ddb_key,create_contract_valid_payload_1) 8 | 9 | 10 | #### Build/Deploy Tasks 11 | ci: clean build deploy 12 | 13 | build: 14 | ruff format 15 | sam validate --lint 16 | cfn-lint template.yaml -a cfn_lint_serverless.rules 17 | uv export --no-hashes --format=requirements-txt --output-file=src/requirements.txt 18 | sam build -c $(DOCKER_OPTS) 19 | 20 | deps: 21 | uv sync 22 | 23 | deploy: deps build 24 | sam deploy 25 | 26 | #### Tests 27 | test: unit-test integration-test 28 | 29 | unit-test: 30 | uv run pytest tests/unit/ 31 | 32 | integration-test: deps 33 | uv run pytest tests/integration/ 34 | 35 | curl-test: clean-tests 36 | $(call runif,CREATE CONTRACT) 37 | $(call mcurl,POST,create_contract_valid_payload_1) 38 | 39 | $(call runif,Query DDB) 40 | $(call ddb_get,$(ddbPropertyId)) 41 | 42 | $(call runif,UPDATE CONTRACT) 43 | $(call mcurl,PUT,update_existing_contract_valid_payload_1) 44 | 45 | $(call runif,Query DDB) 46 | $(call ddb_get,$(ddbPropertyId)) 47 | 48 | $(call runif,Delete DDB Items) 49 | $(MAKE) clean-tests 50 | 51 | @echo "[DONE]" 52 | 53 | 54 | clean-tests: 55 | $(call ddb_delete,$(ddbPropertyId)) || true 56 | 57 | 58 | #### Utilities 59 | sync: 60 | sam sync --stack-name $(stackName) --watch 61 | 62 | logs: 63 | sam logs -t 64 | 65 | clean: 66 | find . -type d -name __pycache__ -exec rm -rf {} \; 2>/dev/null || true 67 | find . -type f -name requirements.txt -exec rm -f {} \; 2>/dev/null || true 68 | rm -rf .pytest_cache/ .aws-sam/ || true 69 | 70 | delete: 71 | sam delete --stack-name $(stackName) --no-prompts 72 | 73 | ci_init: 74 | uv venv 75 | uv export --no-hashes --format=requirements.txt --output-file=src/requirements.txt --extra=dev 76 | uv pip install -r src/requirements.txt 77 | 78 | 79 | #### Helper Functions 80 | define runif 81 | @echo 82 | @echo "Run $(1) now?" 83 | @read 84 | @echo "Running $(1)" 85 | endef 86 | 87 | define ddb_get 88 | @aws dynamodb get-item \ 89 | --table-name $(call cf_output,$(stackName),ContractsTableName) \ 90 | --key '$(1)' \ 91 | | jq -f tests/integration/transformations/ddb_contract.jq 92 | endef 93 | 94 | define ddb_delete 95 | aws dynamodb delete-item \ 96 | --table-name $(call cf_output,$(stackName),ContractsTableName) \ 97 | --key '$(1)' 98 | endef 99 | 100 | define mcurl 101 | curl -X $(1) -H "Content-type: application/json" -d @$(call payload,$(2)) $(apiUrl)contracts 102 | endef 103 | 104 | define get_ddb_key 105 | $(shell jq '. | {property_id:{S:.property_id}}' $(call payload,$(1)) | tr -d ' ') 106 | endef 107 | 108 | define payload 109 | tests/integration/events/$(1).json 110 | endef 111 | 112 | define cf_output 113 | $(shell aws cloudformation describe-stacks \ 114 | --output text \ 115 | --stack-name $(1) \ 116 | --query 'Stacks[0].Outputs[?OutputKey==`$(2)`].OutputValue') 117 | endef 118 | -------------------------------------------------------------------------------- /unicorn_contracts/README.md: -------------------------------------------------------------------------------- 1 | # Developing Unicorn Contracts 2 | 3 | ![Contracts Service Architecture](https://static.us-east-1.prod.workshops.aws/public/f273b5fc-17cd-406b-9e63-1d331b00589d/static/images/architecture-contracts.png) 4 | 5 | ## Architecture overview 6 | 7 | Unicorn Contract manages the contractual relationship between the customers and the Unicorn Properties agency. It's primary function is to allow Unicorn Properties agents to create a new contract for a property listing, and to have the contract approved once it's ready. 8 | 9 | The architecture is fairly straight forward. An API exposes the create contract and update contract methods. This information is recorded in a Amazon DynamoDB table which will contain all latest information about the contract and it's status. 10 | 11 | Each time a new contract is created or updated, Unicorn Contracts publishes a `ContractStatusChanged` event to Amazon EventBridge signalling changes to the contract status. These events are consumed by **Unicorn Properties**, so it can track changes to contracts, without needing to take a direct dependency on Unicorn Contracts and it's database. 12 | 13 | Here is an example of an event that is published to EventBridge: 14 | 15 | ```json 16 | { 17 | "version": "0", 18 | "account": "123456789012", 19 | "region": "us-east-1", 20 | "detail-type": "ContractStatusChanged", 21 | "source": "unicorn.contracts", 22 | "time": "2022-08-14T22:06:31Z", 23 | "id": "c071bfbf-83c4-49ca-a6ff-3df053957145", 24 | "resources": [], 25 | "detail": { 26 | "contract_updated_on": "10/08/2022 19:56:30", 27 | "ContractId": "617dda8c-e79b-406a-bc5b-3a4712f5e4d7", 28 | "PropertyId": "usa/anytown/main-street/111", 29 | "ContractStatus": "DRAFT" 30 | } 31 | } 32 | ``` 33 | 34 | ### Testing the APIs 35 | 36 | ```bash 37 | export API=`aws cloudformation describe-stacks --stack-name uni-prop-local-contract --query "Stacks[0].Outputs[?OutputKey=='ApiUrl'].OutputValue" --output text` 38 | 39 | curl --location --request POST "${API}contract" \ 40 | --header 'Content-Type: application/json' \ 41 | --data-raw '{ 42 | "address": { 43 | "country": "USA", 44 | "city": "Anytown", 45 | "street": "Main Street", 46 | "number": 111 47 | }, 48 | "seller_name": "John Doe", 49 | "property_id": "usa/anytown/main-street/111" 50 | }' 51 | 52 | 53 | curl --location --request PUT "${API}contract" \ 54 | --header 'Content-Type: application/json' \ 55 | --data-raw '{"property_id": "usa/anytown/main-street/111"}' | jq 56 | ``` 57 | -------------------------------------------------------------------------------- /unicorn_contracts/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/aws-serverless-developer-experience-workshop-python/c3e988cd9a2af13b672c1c79d122568333b4e48c/unicorn_contracts/__init__.py -------------------------------------------------------------------------------- /unicorn_contracts/integration/ContractStatusChanged.json: -------------------------------------------------------------------------------- 1 | { 2 | "openapi": "3.0.0", 3 | "info": { 4 | "version": "1.0.0", 5 | "title": "ContractStatusChanged" 6 | }, 7 | "paths": {}, 8 | "components": { 9 | "schemas": { 10 | "AWSEvent": { 11 | "type": "object", 12 | "required": [ 13 | "detail-type", 14 | "resources", 15 | "detail", 16 | "id", 17 | "source", 18 | "time", 19 | "region", 20 | "version", 21 | "account" 22 | ], 23 | "x-amazon-events-detail-type": "ContractStatusChanged", 24 | "x-amazon-events-source": "unicorn.contracts", 25 | "properties": { 26 | "detail": { 27 | "$ref": "#/components/schemas/ContractStatusChanged" 28 | }, 29 | "account": { 30 | "type": "string" 31 | }, 32 | "detail-type": { 33 | "type": "string" 34 | }, 35 | "id": { 36 | "type": "string" 37 | }, 38 | "region": { 39 | "type": "string" 40 | }, 41 | "resources": { 42 | "type": "array", 43 | "items": { 44 | "type": "object" 45 | } 46 | }, 47 | "source": { 48 | "type": "string" 49 | }, 50 | "time": { 51 | "type": "string", 52 | "format": "date-time" 53 | }, 54 | "version": { 55 | "type": "string" 56 | } 57 | } 58 | }, 59 | "ContractStatusChanged": { 60 | "type": "object", 61 | "required": [ 62 | "contract_last_modified_on", 63 | "contract_id", 64 | "contract_status", 65 | "property_id" 66 | ], 67 | "properties": { 68 | "contract_id": { 69 | "type": "string" 70 | }, 71 | "contract_last_modified_on": { 72 | "type": "string", 73 | "format": "date-time" 74 | }, 75 | "contract_status": { 76 | "type": "string" 77 | }, 78 | "property_id": { 79 | "type": "string" 80 | } 81 | } 82 | } 83 | } 84 | } 85 | } -------------------------------------------------------------------------------- /unicorn_contracts/integration/subscriber-policies.yaml: -------------------------------------------------------------------------------- 1 | # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | # SPDX-License-Identifier: MIT-0 3 | AWSTemplateFormatVersion: "2010-09-09" 4 | Description: > 5 | Defines the event bus policies that determine who can create rules on the event bus to 6 | subscribe to events published by the Contracts Service. 7 | 8 | Parameters: 9 | Stage: 10 | Type: String 11 | Default: local 12 | AllowedValues: 13 | - local 14 | - dev 15 | - prod 16 | 17 | Resources: 18 | # This policy defines who can create rules on the event bus. Only principals subscribing to 19 | # Contracts Service events can create rule on the bus. No rules without a defined source. 20 | CrossServiceCreateRulePolicy: 21 | Type: AWS::Events::EventBusPolicy 22 | Properties: 23 | EventBusName: 24 | Fn::Sub: "{{resolve:ssm:/uni-prop/${Stage}/UnicornContractsEventBus}}" 25 | StatementId: 26 | Fn::Sub: "OnlyRulesForContractServiceEvents-${Stage}" 27 | Statement: 28 | Effect: Allow 29 | Principal: 30 | AWS: 31 | Fn::Sub: "arn:${AWS::Partition}:iam::${AWS::AccountId}:root" 32 | Action: 33 | - events:PutRule 34 | - events:DeleteRule 35 | - events:DescribeRule 36 | - events:DisableRule 37 | - events:EnableRule 38 | - events:PutTargets 39 | - events:RemoveTargets 40 | Resource: 41 | - Fn::Sub: 42 | - arn:${AWS::Partition}:events:${AWS::Region}:${AWS::AccountId}:rule/${eventBusName}/* 43 | - eventBusName: 44 | Fn::Sub: "{{resolve:ssm:/uni-prop/${Stage}/UnicornContractsEventBus}}" 45 | Condition: 46 | StringEqualsIfExists: 47 | "events:creatorAccount": "${aws:PrincipalAccount}" 48 | StringEquals: 49 | "events:source": 50 | - "{{resolve:ssm:/uni-prop/UnicornContractsNamespace}}" 51 | "Null": 52 | "events:source": "false" 53 | -------------------------------------------------------------------------------- /unicorn_contracts/pyproject.toml: -------------------------------------------------------------------------------- 1 | [project] 2 | name = "contracts_service" 3 | version = "0.2.0" 4 | description = "Unicorn Properties Contact Service" 5 | authors = [ 6 | {name = "Amazon Web Services"} 7 | ] 8 | readme = "README.md" 9 | requires-python = ">=3.12" 10 | 11 | dependencies = [ 12 | "aws-lambda-powertools[tracer]>=3.9.0", 13 | "aws-xray-sdk>=2.14.0", 14 | "boto3>=1.37.23", 15 | ] 16 | 17 | [project.optional-dependencies] 18 | dev = [ 19 | "aws-lambda-powertools[all]>=3.9.0", 20 | "requests>=2.32.3", 21 | "moto[dynamodb,events,sqs]>=5.0.14", 22 | "importlib-metadata>=8.4.0", 23 | "pyyaml>=6.0.2", 24 | "arnparse>=0.0.2", 25 | "pytest>=8.3.4", 26 | "ruff>=0.9.7", 27 | "tomli>=2.2.1", 28 | ] 29 | 30 | [tool.setuptools] 31 | package-dir = {"contracts_service" = "src"} 32 | packages = ["contracts_service"] 33 | -------------------------------------------------------------------------------- /unicorn_contracts/pytest.ini: -------------------------------------------------------------------------------- 1 | [pytest] 2 | pythonpath = src 3 | -------------------------------------------------------------------------------- /unicorn_contracts/ruff.toml: -------------------------------------------------------------------------------- 1 | # Exclude files/directories from analysis 2 | exclude = [ 3 | ".bzr", 4 | ".direnv", 5 | ".eggs", 6 | ".git", 7 | ".git-rewrite", 8 | ".hg", 9 | ".ipynb_checkpoints", 10 | ".mypy_cache", 11 | ".nox", 12 | ".pants.d", 13 | ".pyenv", 14 | ".pytest_cache", 15 | ".pytype", 16 | ".ruff_cache", 17 | ".svn", 18 | ".tox", 19 | ".venv", 20 | ".vscode", 21 | "__pypackages__", 22 | "_build", 23 | "buck-out", 24 | "build", 25 | "dist", 26 | "node_modules", 27 | "site-packages", 28 | "venv", 29 | ] 30 | 31 | # Same as Black. 32 | line-length = 120 33 | indent-width = 4 34 | 35 | fix = true 36 | 37 | [format] 38 | # Like Black, use double quotes for strings. 39 | quote-style = "double" 40 | 41 | # Like Black, indent with spaces, rather than tabs. 42 | indent-style = "space" 43 | 44 | # Like Black, respect magic trailing commas. 45 | skip-magic-trailing-comma = false 46 | 47 | # Like Black, automatically detect the appropriate line ending. 48 | line-ending = "auto" 49 | 50 | 51 | [lint] 52 | # 1. Enable flake8-bugbear (`B`) rules, in addition to the defaults. 53 | select = ["E4", "E7", "E9", "F", "B"] 54 | 55 | # 2. Avoid enforcing line-length violations (`E501`) 56 | ignore = ["E501"] 57 | 58 | # 3. Avoid trying to fix flake8-bugbear (`B`) violations. 59 | unfixable = ["B"] 60 | 61 | # 4. Ignore `E402` (import violations) in all `__init__.py` files, and in selected subdirectories. 62 | [lint.per-file-ignores] 63 | "__init__.py" = ["E402"] 64 | "**/{tests,docs,tools}/*" = ["E402"] -------------------------------------------------------------------------------- /unicorn_contracts/samconfig.toml: -------------------------------------------------------------------------------- 1 | version = 0.1 2 | 3 | [default.global.parameters] 4 | stack_name = "uni-prop-local-contracts" 5 | s3_prefix = "uni-prop-local-contracts" 6 | resolve_s3 = true 7 | resolve_image_repositories = true 8 | 9 | [default.build.parameters] 10 | cached = true 11 | parallel = true 12 | 13 | [default.deploy.parameters] 14 | disable_rollback = true 15 | confirm_changeset = false 16 | fail_on_empty_changeset = false 17 | capabilities = ["CAPABILITY_IAM", "CAPABILITY_AUTO_EXPAND"] 18 | parameter_overrides = ["Stage=local"] 19 | 20 | [default.validate.parameters] 21 | lint = true 22 | 23 | [default.sync.parameters] 24 | watch = true 25 | 26 | [default.local_start_api.parameters] 27 | warm_containers = "EAGER" 28 | 29 | [default.local_start_lambda.parameters] 30 | warm_containers = "EAGER" -------------------------------------------------------------------------------- /unicorn_contracts/src/contracts_service/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/aws-serverless-developer-experience-workshop-python/c3e988cd9a2af13b672c1c79d122568333b4e48c/unicorn_contracts/src/contracts_service/__init__.py -------------------------------------------------------------------------------- /unicorn_contracts/src/contracts_service/enums.py: -------------------------------------------------------------------------------- 1 | # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | # SPDX-License-Identifier: MIT-0 3 | 4 | from enum import Enum 5 | 6 | 7 | class ContractStatus(Enum): 8 | """Contract status Enum 9 | 10 | APPROVED The contract record is approved. 11 | CANCELLED The contract record is canceled or terminated. You cannot modify a contract record that has this status value. 12 | CLOSED The contract record is closed and all its terms and conditions are met. 13 | You cannot modify a contract record that has this status value. 14 | DRAFT The contract is a draft. 15 | EXPIRED The contract record is expired. The end date for the contract has passed. 16 | You cannot modify a contract record that has this status value. 17 | You can change the status from expire to pending revision by revising the expired contract. 18 | 19 | Parameters 20 | ---------- 21 | Enum : _type_ 22 | _description_ 23 | """ 24 | 25 | APPROVED = 1 26 | CANCELLED = 2 27 | CLOSED = 3 28 | DRAFT = 4 29 | EXPIRED = 5 30 | -------------------------------------------------------------------------------- /unicorn_contracts/src/contracts_service/exceptions.py: -------------------------------------------------------------------------------- 1 | # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | # SPDX-License-Identifier: MIT-0 3 | 4 | import json 5 | 6 | 7 | class ContractNotFoundException(Exception): 8 | """ 9 | Custom exception for encapsulating exceptions for Lambda handler 10 | """ 11 | 12 | def __init__(self, message=None, status_code=None, details=None): 13 | super(ContractNotFoundException, self).__init__() 14 | 15 | self.message = message or "No contract found for specified Property ID" 16 | self.status_code = status_code or 400 17 | self.details = details or {} 18 | 19 | self.apigw_return = {"statusCode": self.status_code, "body": json.dumps({"message": self.message})} 20 | -------------------------------------------------------------------------------- /unicorn_contracts/tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/aws-serverless-developer-experience-workshop-python/c3e988cd9a2af13b672c1c79d122568333b4e48c/unicorn_contracts/tests/__init__.py -------------------------------------------------------------------------------- /unicorn_contracts/tests/integration/__init__.py: -------------------------------------------------------------------------------- 1 | # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | # SPDX-License-Identifier: MIT-0 3 | from typing import Iterator 4 | 5 | import json 6 | import tomli 7 | from pathlib import Path 8 | 9 | import boto3 10 | from arnparse import arnparse 11 | 12 | 13 | #### CONSTANTS 14 | DEFAULT_SAM_CONFIG_FILE = Path(__file__).parent.parent.parent.resolve() / "samconfig.toml" 15 | STACK_OUTPUTS = dict() 16 | EVENTS_DIR = Path(__file__).parent / "events" 17 | 18 | 19 | #### AWS SDK Objects 20 | cfn = boto3.client("cloudformation") 21 | cwl = boto3.client("logs") 22 | ddb = boto3.client("dynamodb") 23 | 24 | 25 | def get_stack_name(samconfig: Path | str = DEFAULT_SAM_CONFIG_FILE) -> str: 26 | with open(samconfig, "rb") as f: 27 | conf = tomli.load(f) 28 | stack_name = conf["default"]["global"]["parameters"]["stack_name"] 29 | 30 | return stack_name 31 | 32 | 33 | def get_stack_output(output_name: str, stack_name: str = get_stack_name()) -> str: 34 | """ 35 | Get the value of an output 36 | """ 37 | 38 | if not (outputs := STACK_OUTPUTS.get(stack_name, dict())): 39 | try: 40 | response = cfn.describe_stacks(StackName=stack_name) 41 | except Exception as e: 42 | raise Exception(f'Cannot find stack {stack_name}. \nPlease make sure stack "{stack_name}" exists.') from e 43 | 44 | outputs = {o["OutputKey"]: o["OutputValue"] for o in response["Stacks"][0]["Outputs"]} 45 | STACK_OUTPUTS[stack_name] = outputs 46 | 47 | try: 48 | return outputs[output_name] 49 | except KeyError as e: 50 | raise Exception(f"Unable to find Output {output_name} on stack {stack_name}") from e 51 | 52 | 53 | def get_event_payload(file) -> dict: 54 | return json.load(open(EVENTS_DIR / f"{file}.json", "r")) 55 | 56 | 57 | def override_payload_number(p: dict, number: int) -> dict: 58 | p["address"]["number"] = number 59 | a = p["address"] 60 | p["property_id"] = f"{a['country']}/{a['city']}/{a['street']}/{a['number']}".replace(" ", "-").lower() 61 | return p 62 | 63 | 64 | def get_cw_logs_values(eb_log_group_arn: str, property_id: str) -> Iterator[dict]: 65 | group_name = arnparse(eb_log_group_arn).resource 66 | 67 | # Get the CW LogStream with the latest log messages 68 | stream_response = cwl.describe_log_streams( 69 | logGroupName=group_name, orderBy="LastEventTime", descending=True, limit=3 70 | ) 71 | latestlogStreamNames = [s["logStreamName"] for s in stream_response["logStreams"]] 72 | # Fetch log events from that stream 73 | responses = [cwl.get_log_events(logGroupName=group_name, logStreamName=name) for name in latestlogStreamNames] 74 | 75 | # Filter log events that match the required `property_id` 76 | for response in responses: 77 | for event in response["events"]: 78 | if (ev := json.loads(event["message"])).get("detail", {}).get("property_id", "") == property_id: 79 | yield ev 80 | 81 | 82 | def clean_ddb(table_name, property_id): 83 | ddb.delete_item(TableName=table_name, Key={"property_id": {"S": property_id}}) 84 | -------------------------------------------------------------------------------- /unicorn_contracts/tests/integration/events/create_contract_invalid_payload_1.json: -------------------------------------------------------------------------------- 1 | { 2 | "add": "St.1 , Building 10", 3 | "sell": "John Smith", 4 | "prop": "4781231c-bc30-4f30-8b30-7145f4dd1adb" 5 | } -------------------------------------------------------------------------------- /unicorn_contracts/tests/integration/events/create_contract_valid_payload_1.json: -------------------------------------------------------------------------------- 1 | { 2 | "address": { 3 | "country": "USA", 4 | "city": "Anytown", 5 | "street": "Main Street", 6 | "number": 123 7 | }, 8 | "seller_name": "John Smith", 9 | "property_id": "usa/anytown/main-street/123" 10 | } -------------------------------------------------------------------------------- /unicorn_contracts/tests/integration/events/update_existing_contract_invalid_payload_1.json: -------------------------------------------------------------------------------- 1 | { 2 | "property_id": "usa/anytown/main-street/123", 3 | "add": "St.1 , Building 10", 4 | "sell": "John Smith", 5 | "prop": "4781231c-bc30-4f30-8b30-7145f4dd1adb" 6 | } -------------------------------------------------------------------------------- /unicorn_contracts/tests/integration/events/update_existing_contract_valid_payload_1.json: -------------------------------------------------------------------------------- 1 | { 2 | "property_id": "usa/anytown/main-street/123" 3 | } -------------------------------------------------------------------------------- /unicorn_contracts/tests/integration/events/update_missing_contract_invalid_payload_1.json: -------------------------------------------------------------------------------- 1 | { 2 | "add": "St.1 , Building 10", 3 | "sell": "John Smith", 4 | "prop": "4781231c-bc30-4f30-8b30-7145f4dd1adb" 5 | } -------------------------------------------------------------------------------- /unicorn_contracts/tests/integration/events/update_missing_contract_valid_payload_1.json: -------------------------------------------------------------------------------- 1 | { 2 | "property_id": "usa/some_other_town/street/878828" 3 | } -------------------------------------------------------------------------------- /unicorn_contracts/tests/integration/test_create_contract_apigw.py: -------------------------------------------------------------------------------- 1 | # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | # SPDX-License-Identifier: MIT-0 3 | from typing import List 4 | 5 | from time import sleep 6 | from random import randint 7 | 8 | import requests 9 | from unittest import TestCase 10 | 11 | from . import get_stack_output, get_cw_logs_values, clean_ddb 12 | from . import get_event_payload, override_payload_number 13 | 14 | 15 | class TestCreateContract(TestCase): 16 | api_endpoint: str 17 | eb_log_group: str 18 | contracts_table: str 19 | properties: List[str] 20 | 21 | def setUp(self) -> None: 22 | self.api_endpoint = get_stack_output("ApiUrl") 23 | self.eb_log_group = get_stack_output("UnicornContractsCatchAllLogGroupArn").rstrip(":*") 24 | self.contracts_table = get_stack_output("ContractsTableName") 25 | self.properties = list() 26 | 27 | def tearDown(self) -> None: 28 | for i in self.properties: 29 | clean_ddb(self.contracts_table, i) 30 | 31 | def test_create_contract_invalid_payload_1(self): 32 | """ 33 | Call the API Gateway endpoint and check the response 34 | """ 35 | 36 | payload = get_event_payload("create_contract_invalid_payload_1") 37 | response = requests.post(f"{self.api_endpoint}contracts", json=payload) 38 | self.assertEqual(response.status_code, 400) 39 | self.assertDictEqual(response.json(), response.json() | {"message": "Invalid request body"}) 40 | 41 | def test_create_contract_valid_payload_1(self): 42 | prop_number = randint(1, 9999) 43 | payload = override_payload_number(get_event_payload("create_contract_valid_payload_1"), prop_number) 44 | 45 | # Call API to create new Contract 46 | response = requests.post(f"{self.api_endpoint}contracts", json=payload) 47 | self.properties.append(payload["property_id"]) 48 | 49 | self.assertEqual(response.status_code, 200) 50 | self.assertDictEqual(response.json(), response.json() | {"message": "OK"}) 51 | 52 | sleep(5) 53 | try: 54 | eb_event = next(get_cw_logs_values(self.eb_log_group, payload["property_id"])) 55 | except Exception: 56 | raise Exception(f"Unable to get EventBridge Event from CloudWatch Logs group {self.eb_log_group}") 57 | 58 | self.assertEqual(eb_event["detail"]["contract_status"], "DRAFT") 59 | -------------------------------------------------------------------------------- /unicorn_contracts/tests/integration/test_update_contract_apigw.py: -------------------------------------------------------------------------------- 1 | # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | # SPDX-License-Identifier: MIT-0 3 | from typing import List 4 | 5 | from time import sleep 6 | from random import randint 7 | 8 | import requests 9 | from unittest import TestCase 10 | 11 | from . import get_stack_output, get_cw_logs_values, clean_ddb 12 | from . import get_event_payload, override_payload_number 13 | 14 | 15 | class TestUpdateContract(TestCase): 16 | api_endpoint: str 17 | eb_log_group: str 18 | contracts_table: str 19 | properties: List[str] 20 | 21 | def setUp(self) -> None: 22 | self.api_endpoint = get_stack_output("ApiUrl") 23 | self.eb_log_group = get_stack_output("UnicornContractsCatchAllLogGroupArn").rstrip(":*") 24 | self.contracts_table = get_stack_output("ContractsTableName") 25 | self.properties = list() 26 | 27 | def tearDown(self) -> None: 28 | for i in self.properties: 29 | clean_ddb(self.contracts_table, i) 30 | 31 | # NOTE: This test is not working as it supposed to. 32 | # Need a way for OpenApi Spec to validate extra keys on payload 33 | # def test_update_existing_contract_invalid_payload_1(self): 34 | # payload = get_event_payload('update_existing_contract_invalid_payload_1') 35 | 36 | # response = requests.put(f'{self.api_endpoint}contracts', json=payload) 37 | # self.assertEqual(response.status_code, 400) 38 | 39 | def test_update_existing_contract_valid_payload(self): 40 | prop_number = randint(1, 9999) 41 | payload = override_payload_number(get_event_payload("create_contract_valid_payload_1"), prop_number) 42 | 43 | # Call API to create new Contract 44 | response = requests.post(f"{self.api_endpoint}contracts", json=payload) 45 | self.properties.append(payload["property_id"]) 46 | 47 | # Call API to update contract 48 | response = requests.put(f"{self.api_endpoint}contracts", json={"property_id": payload["property_id"]}) 49 | self.assertEqual(response.status_code, 200) 50 | self.assertDictEqual(response.json(), response.json() | {"message": "OK"}) 51 | 52 | sleep(10) 53 | try: 54 | events_contract_statuses = [ 55 | e["detail"]["contract_status"] for e in get_cw_logs_values(self.eb_log_group, payload["property_id"]) 56 | ] 57 | events_contract_statuses.sort() 58 | except Exception: 59 | raise Exception(f"Unable to get EventBridge Event from CloudWatch Logs group {self.eb_log_group}") 60 | 61 | # self.assertTrue("APPROVED" in events_contract_statuses) 62 | self.assertListEqual(events_contract_statuses, ["APPROVED", "DRAFT"]) 63 | 64 | def test_update_missing_contract_invalid_payload_1(self): 65 | payload = {"add": "St.1 , Building 10", "sell": "John Smith", "prop": "4781231c-bc30-4f30-8b30-7145f4dd1adb"} 66 | 67 | response = requests.put(f"{self.api_endpoint}contracts", json=payload) 68 | self.assertEqual(response.status_code, 400) 69 | self.assertDictEqual(response.json(), response.json() | {"message": "Invalid request body"}) 70 | 71 | def test_update_missing_contract_valid_payload(self): 72 | payload = {"property_id": "usa/some_other_town/street/878828"} 73 | 74 | response = requests.put(f"{self.api_endpoint}contracts", json=payload) 75 | self.assertEqual(response.status_code, 200) 76 | self.assertDictEqual(response.json(), response.json() | {"message": "OK"}) 77 | -------------------------------------------------------------------------------- /unicorn_contracts/tests/integration/transformations/ddb_contract.jq: -------------------------------------------------------------------------------- 1 | .Item | { 2 | property_id: .property_id.S, 3 | contract_id: .contract_id.S, 4 | seller_name: .seller_name.S, 5 | address: { 6 | country: .address.M.country.S, 7 | number: .address.M.number.N, 8 | city: .address.M.city.S, 9 | street: .address.M.street.S, 10 | }, 11 | contract_status: .contract_status.S, 12 | contract_created: .contract_created.S, 13 | contract_last_modified_on: .contract_last_modified_on.S 14 | } | del(..|nulls) 15 | -------------------------------------------------------------------------------- /unicorn_contracts/tests/pipes/event_bridge_payloads/create_contract.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0", 3 | "id": "ab4c2d2e-f483-46e6-54b4-96a02950a556", 4 | "detail-type": "ContractStatusChanged", 5 | "source": "unicorn.contracts", 6 | "account": "718758479978", 7 | "time": "2023-08-25T04:59:40Z", 8 | "region": "ap-southeast-2", 9 | "resources": [], 10 | "detail": { 11 | "contract_id": "d63b341c-cf87-428d-b6ca-5e789bfdfc14", 12 | "contract_last_modified_on": "25/08/2023 04:59:40", 13 | "contract_status": "DRAFT", 14 | "property_id": "usa/anytown/main-street/123" 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /unicorn_contracts/tests/pipes/event_bridge_payloads/update_contract.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0", 3 | "id": "0301025e-92e5-c036-534f-f5adbf8cb867", 4 | "detail-type": "ContractStatusChanged", 5 | "source": "unicorn.contracts", 6 | "account": "718758479978", 7 | "time": "2023-08-25T05:00:23Z", 8 | "region": "ap-southeast-2", 9 | "resources": [], 10 | "detail": { 11 | "contract_id": "d63b341c-cf87-428d-b6ca-5e789bfdfc14", 12 | "contract_last_modified_on": "25/08/2023 04:59:40", 13 | "contract_status": "APPROVED", 14 | "property_id": "usa/anytown/main-street/123" 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /unicorn_contracts/tests/pipes/pipes_payloads/create_contract.json: -------------------------------------------------------------------------------- 1 | { 2 | "eventID": "f596bdbb621c57e694189ae9f1c172c2", 3 | "eventName": "INSERT", 4 | "eventVersion": "1.1", 5 | "eventSource": "aws:dynamodb", 6 | "awsRegion": "ap-southeast-2", 7 | "dynamodb": { 8 | "ApproximateCreationDateTime": 1692929660, 9 | "Keys": { 10 | "property_id": { 11 | "S": "usa/anytown/main-street/123" 12 | } 13 | }, 14 | "NewImage": { 15 | "contract_last_modified_on": { 16 | "S": "25/08/2023 02:14:20" 17 | }, 18 | "address": { 19 | "M": { 20 | "country": { 21 | "S": "USA" 22 | }, 23 | "number": { 24 | "N": "123" 25 | }, 26 | "city": { 27 | "S": "Anytown" 28 | }, 29 | "street": { 30 | "S": "Main Street" 31 | } 32 | } 33 | }, 34 | "seller_name": { 35 | "S": "John Smith" 36 | }, 37 | "contract_created": { 38 | "S": "25/08/2023 02:14:20" 39 | }, 40 | "contract_id": { 41 | "S": "5bb04023-74aa-41fc-b86b-447602759270" 42 | }, 43 | "contract_status": { 44 | "S": "DRAFT" 45 | }, 46 | "property_id": { 47 | "S": "usa/anytown/main-street/123" 48 | } 49 | }, 50 | "SequenceNumber": "4800600000000041815691506", 51 | "SizeBytes": 303, 52 | "StreamViewType": "NEW_AND_OLD_IMAGES" 53 | }, 54 | "eventSourceARN": "arn:aws:dynamodb:ap-southeast-2:718758479978:table/uni-prop-local-contract-ContractsTable-JKAROODQJH0P/stream/2023-08-24T00:35:44.603" 55 | } -------------------------------------------------------------------------------- /unicorn_contracts/tests/pipes/pipes_payloads/update_contract.json: -------------------------------------------------------------------------------- 1 | { 2 | "eventID": "54e72fec04d65113c9bc5905a3e3a18c", 3 | "eventName": "MODIFY", 4 | "eventVersion": "1.1", 5 | "eventSource": "aws:dynamodb", 6 | "awsRegion": "ap-southeast-2", 7 | "dynamodb": { 8 | "ApproximateCreationDateTime": 1692929694, 9 | "Keys": { 10 | "property_id": { 11 | "S": "usa/anytown/main-street/123" 12 | } 13 | }, 14 | "NewImage": { 15 | "contract_last_modified_on": { 16 | "S": "25/08/2023 02:14:20" 17 | }, 18 | "address": { 19 | "M": { 20 | "country": { 21 | "S": "USA" 22 | }, 23 | "number": { 24 | "N": "123" 25 | }, 26 | "city": { 27 | "S": "Anytown" 28 | }, 29 | "street": { 30 | "S": "Main Street" 31 | } 32 | } 33 | }, 34 | "seller_name": { 35 | "S": "John Smith" 36 | }, 37 | "contract_created": { 38 | "S": "25/08/2023 02:14:20" 39 | }, 40 | "contract_id": { 41 | "S": "5bb04023-74aa-41fc-b86b-447602759270" 42 | }, 43 | "contract_status": { 44 | "S": "APPROVED" 45 | }, 46 | "modified_date": { 47 | "S": "25/08/2023 02:14:54" 48 | }, 49 | "property_id": { 50 | "S": "usa/anytown/main-street/123" 51 | } 52 | }, 53 | "OldImage": { 54 | "contract_last_modified_on": { 55 | "S": "25/08/2023 02:14:20" 56 | }, 57 | "address": { 58 | "M": { 59 | "country": { 60 | "S": "USA" 61 | }, 62 | "number": { 63 | "N": "123" 64 | }, 65 | "city": { 66 | "S": "Anytown" 67 | }, 68 | "street": { 69 | "S": "Main Street" 70 | } 71 | } 72 | }, 73 | "seller_name": { 74 | "S": "John Smith" 75 | }, 76 | "contract_created": { 77 | "S": "25/08/2023 02:14:20" 78 | }, 79 | "contract_id": { 80 | "S": "5bb04023-74aa-41fc-b86b-447602759270" 81 | }, 82 | "contract_status": { 83 | "S": "DRAFT" 84 | }, 85 | "property_id": { 86 | "S": "usa/anytown/main-street/123" 87 | } 88 | }, 89 | "SequenceNumber": "4800700000000041815709335", 90 | "SizeBytes": 603, 91 | "StreamViewType": "NEW_AND_OLD_IMAGES" 92 | }, 93 | "eventSourceARN": "arn:aws:dynamodb:ap-southeast-2:718758479978:table/uni-prop-local-contract-ContractsTable-JKAROODQJH0P/stream/2023-08-24T00:35:44.603" 94 | } -------------------------------------------------------------------------------- /unicorn_contracts/tests/pipes/streams_payloads/create_contract.json: -------------------------------------------------------------------------------- 1 | { 2 | "ApproximateCreationDateTime": 1692929660, 3 | "Keys": { 4 | "property_id": { 5 | "S": "usa/anytown/main-street/123" 6 | } 7 | }, 8 | "NewImage": { 9 | "contract_last_modified_on": { 10 | "S": "25/08/2023 02:14:20" 11 | }, 12 | "address": { 13 | "M": { 14 | "country": { 15 | "S": "USA" 16 | }, 17 | "number": { 18 | "N": "123" 19 | }, 20 | "city": { 21 | "S": "Anytown" 22 | }, 23 | "street": { 24 | "S": "Main Street" 25 | } 26 | } 27 | }, 28 | "seller_name": { 29 | "S": "John Smith" 30 | }, 31 | "contract_created": { 32 | "S": "25/08/2023 02:14:20" 33 | }, 34 | "contract_id": { 35 | "S": "5bb04023-74aa-41fc-b86b-447602759270" 36 | }, 37 | "contract_status": { 38 | "S": "DRAFT" 39 | }, 40 | "property_id": { 41 | "S": "usa/anytown/main-street/123" 42 | } 43 | }, 44 | "SequenceNumber": "4800600000000041815691506", 45 | "SizeBytes": 303, 46 | "StreamViewType": "NEW_AND_OLD_IMAGES" 47 | } -------------------------------------------------------------------------------- /unicorn_contracts/tests/pipes/streams_payloads/delete_contract.json: -------------------------------------------------------------------------------- 1 | { 2 | "ApproximateCreationDateTime": 1692929729, 3 | "Keys": { 4 | "property_id": { 5 | "S": "usa/anytown/main-street/123" 6 | } 7 | }, 8 | "OldImage": { 9 | "contract_last_modified_on": { 10 | "S": "25/08/2023 02:14:20" 11 | }, 12 | "address": { 13 | "M": { 14 | "country": { 15 | "S": "USA" 16 | }, 17 | "number": { 18 | "N": "123" 19 | }, 20 | "city": { 21 | "S": "Anytown" 22 | }, 23 | "street": { 24 | "S": "Main Street" 25 | } 26 | } 27 | }, 28 | "seller_name": { 29 | "S": "John Smith" 30 | }, 31 | "contract_created": { 32 | "S": "25/08/2023 02:14:20" 33 | }, 34 | "contract_id": { 35 | "S": "5bb04023-74aa-41fc-b86b-447602759270" 36 | }, 37 | "contract_status": { 38 | "S": "APPROVED" 39 | }, 40 | "modified_date": { 41 | "S": "25/08/2023 02:14:54" 42 | }, 43 | "property_id": { 44 | "S": "usa/anytown/main-street/123" 45 | } 46 | }, 47 | "SequenceNumber": "4800800000000041815727252", 48 | "SizeBytes": 338, 49 | "StreamViewType": "NEW_AND_OLD_IMAGES" 50 | } -------------------------------------------------------------------------------- /unicorn_contracts/tests/pipes/streams_payloads/update_contract.json: -------------------------------------------------------------------------------- 1 | { 2 | "ApproximateCreationDateTime": 1692929694, 3 | "Keys": { 4 | "property_id": { 5 | "S": "usa/anytown/main-street/123" 6 | } 7 | }, 8 | "NewImage": { 9 | "contract_last_modified_on": { 10 | "S": "25/08/2023 02:14:20" 11 | }, 12 | "address": { 13 | "M": { 14 | "country": { 15 | "S": "USA" 16 | }, 17 | "number": { 18 | "N": "123" 19 | }, 20 | "city": { 21 | "S": "Anytown" 22 | }, 23 | "street": { 24 | "S": "Main Street" 25 | } 26 | } 27 | }, 28 | "seller_name": { 29 | "S": "John Smith" 30 | }, 31 | "contract_created": { 32 | "S": "25/08/2023 02:14:20" 33 | }, 34 | "contract_id": { 35 | "S": "5bb04023-74aa-41fc-b86b-447602759270" 36 | }, 37 | "contract_status": { 38 | "S": "APPROVED" 39 | }, 40 | "modified_date": { 41 | "S": "25/08/2023 02:14:54" 42 | }, 43 | "property_id": { 44 | "S": "usa/anytown/main-street/123" 45 | } 46 | }, 47 | "OldImage": { 48 | "contract_last_modified_on": { 49 | "S": "25/08/2023 02:14:20" 50 | }, 51 | "address": { 52 | "M": { 53 | "country": { 54 | "S": "USA" 55 | }, 56 | "number": { 57 | "N": "123" 58 | }, 59 | "city": { 60 | "S": "Anytown" 61 | }, 62 | "street": { 63 | "S": "Main Street" 64 | } 65 | } 66 | }, 67 | "seller_name": { 68 | "S": "John Smith" 69 | }, 70 | "contract_created": { 71 | "S": "25/08/2023 02:14:20" 72 | }, 73 | "contract_id": { 74 | "S": "5bb04023-74aa-41fc-b86b-447602759270" 75 | }, 76 | "contract_status": { 77 | "S": "DRAFT" 78 | }, 79 | "property_id": { 80 | "S": "usa/anytown/main-street/123" 81 | } 82 | }, 83 | "SequenceNumber": "4800700000000041815709335", 84 | "SizeBytes": 603, 85 | "StreamViewType": "NEW_AND_OLD_IMAGES" 86 | } -------------------------------------------------------------------------------- /unicorn_contracts/tests/unit/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/aws-serverless-developer-experience-workshop-python/c3e988cd9a2af13b672c1c79d122568333b4e48c/unicorn_contracts/tests/unit/__init__.py -------------------------------------------------------------------------------- /unicorn_contracts/tests/unit/conftest.py: -------------------------------------------------------------------------------- 1 | # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | # SPDX-License-Identifier: MIT-0 3 | import os 4 | 5 | import boto3 6 | from aws_lambda_powertools.utilities.typing import LambdaContext 7 | 8 | import pytest 9 | from moto import mock_aws 10 | 11 | 12 | @pytest.fixture(scope="function") 13 | def aws_credentials(): 14 | """Mocked AWS Credentials for moto.""" 15 | os.environ["AWS_ACCESS_KEY_ID"] = "testing" 16 | os.environ["AWS_SECRET_ACCESS_KEY"] = "testing" 17 | os.environ["AWS_SECURITY_TOKEN"] = "testing" 18 | os.environ["AWS_SESSION_TOKEN"] = "testing" 19 | 20 | 21 | @pytest.fixture(scope="function") 22 | def dynamodb(aws_credentials): 23 | with mock_aws(): 24 | yield boto3.resource("dynamodb", region_name="ap-southeast-2") 25 | 26 | 27 | @pytest.fixture(scope="function") 28 | def eventbridge(aws_credentials): 29 | with mock_aws(): 30 | yield boto3.client("events", region_name="ap-southeast-2") 31 | 32 | 33 | @pytest.fixture(scope="function") 34 | def sqs(aws_credentials): 35 | with mock_aws(): 36 | yield boto3.client("sqs", region_name="ap-southeast-2") 37 | 38 | 39 | @pytest.fixture(scope="function") 40 | def lambda_context(): 41 | context: LambdaContext = LambdaContext() 42 | context._function_name = "contractsService-LambdaFunction-IWaQgsTEtLtX" 43 | context._function_version = "$LATEST" 44 | context._invoked_function_arn = ( 45 | "arn:aws:lambda:ap-southeast-2:424490683636:function:contractsService-LambdaFunction-IWaQgsTEtLtX" 46 | ) 47 | context._memory_limit_in_mb = 128 48 | context._aws_request_id = "6f970d26-71d6-4c87-a196-9375f85c7b07" 49 | context._log_group_name = "/aws/lambda/contractsService-LambdaFunction-IWaQgsTEtLtX" 50 | context._log_stream_name = "2022/07/14/[$LATEST]7c71ca59882b4c569dd007c7e41c81e8" 51 | # context._identity=CognitoIdentity([cognito_identity_id=None,cognito_identity_pool_id=None])]) 52 | # context._client_context=None 53 | return context 54 | -------------------------------------------------------------------------------- /unicorn_contracts/tests/unit/events/create_contract_invalid_1.json: -------------------------------------------------------------------------------- 1 | { 2 | "add": "St.1 , Building 10", 3 | "seller": "John Smith", 4 | "property": "4781231c-bc30-4f30-8b30-7145f4dd1adb" 5 | } 6 | -------------------------------------------------------------------------------- /unicorn_contracts/tests/unit/events/create_contract_valid_1.json: -------------------------------------------------------------------------------- 1 | { 2 | "address": { 3 | "country": "USA", 4 | "city": "Anytown", 5 | "street": "Main Street", 6 | "number": 123 7 | }, 8 | "seller_name": "John Smith", 9 | "property_id": "usa/anytown/main-street/123" 10 | } 11 | -------------------------------------------------------------------------------- /unicorn_contracts/tests/unit/events/update_contract_valid_1.json: -------------------------------------------------------------------------------- 1 | { 2 | "property_id": "usa/anytown/main-street/123" 3 | } -------------------------------------------------------------------------------- /unicorn_contracts/tests/unit/helper.py: -------------------------------------------------------------------------------- 1 | # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | # SPDX-License-Identifier: MIT-0 3 | import json 4 | from pathlib import Path 5 | 6 | 7 | TABLE_NAME = "table1" 8 | EVENTBUS_NAME = "test-eventbridge" 9 | SQS_QUEUE_NAME = "test_sqs" 10 | EVENTS_DIR = Path(__file__).parent / "events" 11 | 12 | 13 | def load_event(filename): 14 | return json.load(open(EVENTS_DIR / f"{filename}.json", "r")) 15 | 16 | 17 | def return_env_vars_dict(k=None): 18 | if k is None: 19 | k = {} 20 | 21 | env_dict = { 22 | "AWS_DEFAULT_REGION": "ap-southeast-2", 23 | "EVENT_BUS": EVENTBUS_NAME, 24 | "DYNAMODB_TABLE": TABLE_NAME, 25 | "SERVICE_NAMESPACE": "unicorn.contracts", 26 | "POWERTOOLS_LOGGER_CASE": "PascalCase", 27 | "POWERTOOLS_SERVICE_NAME": "unicorn.contracts", 28 | "POWERTOOLS_TRACE_DISABLED": "true", 29 | "POWERTOOLS_LOGGER_LOG_EVENT": "true", 30 | "POWERTOOLS_LOGGER_SAMPLE_RATE": "0.1", 31 | "POWERTOOLS_METRICS_NAMESPACE": "unicorn.contracts", 32 | "LOG_LEVEL": "INFO", 33 | } 34 | 35 | env_dict |= k 36 | 37 | return env_dict 38 | 39 | 40 | def create_ddb_table_contracts(dynamodb): 41 | table = dynamodb.create_table( 42 | TableName=TABLE_NAME, 43 | KeySchema=[{"AttributeName": "property_id", "KeyType": "HASH"}], 44 | AttributeDefinitions=[ 45 | {"AttributeName": "property_id", "AttributeType": "S"}, 46 | ], 47 | ProvisionedThroughput={"ReadCapacityUnits": 1, "WriteCapacityUnits": 1}, 48 | ) 49 | table.meta.client.get_waiter("table_exists").wait(TableName=TABLE_NAME) 50 | return table 51 | 52 | 53 | def create_ddb_table_contracts_with_entry(dynamodb): 54 | table = dynamodb.create_table( 55 | TableName=TABLE_NAME, 56 | KeySchema=[{"AttributeName": "property_id", "KeyType": "HASH"}], 57 | AttributeDefinitions=[ 58 | {"AttributeName": "property_id", "AttributeType": "S"}, 59 | ], 60 | ProvisionedThroughput={"ReadCapacityUnits": 1, "WriteCapacityUnits": 1}, 61 | ) 62 | table.meta.client.get_waiter("table_exists").wait(TableName=TABLE_NAME) 63 | contract = { 64 | "property_id": "usa/anytown/main-street/123", # PK 65 | "contract_created": "01/08/2022 20:36:30", 66 | "contract_last_modified_on": "01/08/2022 20:36:30", 67 | "contract_id": "11111111", 68 | "address": {"country": "USA", "city": "Anytown", "street": "Main Street", "number": 123}, 69 | "seller_name": "John Smith", 70 | "contract_status": "DRAFT", 71 | } 72 | table.put_item(Item=contract) 73 | return table 74 | 75 | 76 | def create_test_eventbridge_bus(eventbridge): 77 | bus = eventbridge.create_event_bus(Name=EVENTBUS_NAME) 78 | return bus 79 | 80 | 81 | def create_test_sqs_ingestion_queue(sqs): 82 | queue = sqs.create_queue(QueueName=SQS_QUEUE_NAME) 83 | return queue 84 | -------------------------------------------------------------------------------- /unicorn_contracts/tests/unit/test_contract_event_handler.py: -------------------------------------------------------------------------------- 1 | # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | # SPDX-License-Identifier: MIT-0 3 | import os 4 | from importlib import reload 5 | 6 | import pytest 7 | from unittest import mock 8 | from botocore.exceptions import ClientError 9 | 10 | from .event_generator import sqs_event 11 | from .helper import TABLE_NAME 12 | from .helper import load_event, return_env_vars_dict 13 | from .helper import create_ddb_table_contracts, create_test_sqs_ingestion_queue, create_ddb_table_contracts_with_entry 14 | 15 | 16 | @mock.patch.dict(os.environ, return_env_vars_dict(), clear=True) 17 | def test_valid_create_event(dynamodb, sqs, lambda_context): 18 | payload = load_event("create_contract_valid_1") 19 | event = sqs_event([{"body": payload, "attributes": {"HttpMethod": "POST"}}]) 20 | 21 | # Loading function here so that mocking works correctly. 22 | from contracts_service import contract_event_handler # noqa: F401 23 | 24 | # Reload is required to prevent function setup reuse from another test 25 | reload(contract_event_handler) 26 | 27 | create_ddb_table_contracts(dynamodb) 28 | create_test_sqs_ingestion_queue(sqs) 29 | 30 | contract_event_handler.lambda_handler(event, lambda_context) 31 | 32 | res = dynamodb.Table(TABLE_NAME).get_item(Key={"property_id": payload["property_id"]}) 33 | 34 | assert res["Item"]["property_id"] == payload["property_id"] 35 | assert res["Item"]["contract_status"] == "DRAFT" 36 | 37 | assert res["Item"]["seller_name"] == payload["seller_name"] 38 | assert res["Item"]["address"]["country"] == payload["address"]["country"] 39 | assert res["Item"]["address"]["city"] == payload["address"]["city"] 40 | assert res["Item"]["address"]["street"] == payload["address"]["street"] 41 | assert res["Item"]["address"]["number"] == payload["address"]["number"] 42 | 43 | 44 | @mock.patch.dict(os.environ, return_env_vars_dict(), clear=True) 45 | def test_valid_update_event(dynamodb, sqs, lambda_context): 46 | payload = load_event("update_contract_valid_1") 47 | event = sqs_event([{"body": payload, "attributes": {"HttpMethod": "PUT"}}]) 48 | 49 | # Loading function here so that mocking works correctly. 50 | from contracts_service import contract_event_handler # noqa: F401 51 | 52 | # Reload is required to prevent function setup reuse from another test 53 | reload(contract_event_handler) 54 | 55 | create_ddb_table_contracts_with_entry(dynamodb) 56 | create_test_sqs_ingestion_queue(sqs) 57 | 58 | contract_event_handler.lambda_handler(event, lambda_context) 59 | 60 | res = dynamodb.Table(TABLE_NAME).get_item(Key={"property_id": payload["property_id"]}) 61 | 62 | assert res["Item"]["property_id"] == payload["property_id"] 63 | assert res["Item"]["contract_status"] == "APPROVED" 64 | 65 | 66 | @mock.patch.dict(os.environ, return_env_vars_dict(), clear=True) 67 | def test_missing_ddb_env_var(): 68 | del os.environ["DYNAMODB_TABLE"] 69 | # Loading function here so that mocking works correctly 70 | with pytest.raises(EnvironmentError): 71 | from contracts_service import contract_event_handler 72 | 73 | reload(contract_event_handler) 74 | 75 | 76 | @mock.patch.dict(os.environ, return_env_vars_dict(), clear=True) 77 | def test_missing_sm_env_var(): 78 | del os.environ["SERVICE_NAMESPACE"] 79 | 80 | with pytest.raises(EnvironmentError): 81 | from contracts_service import contract_event_handler 82 | 83 | reload(contract_event_handler) 84 | 85 | 86 | @mock.patch.dict(os.environ, return_env_vars_dict({"DYNAMODB_TABLE": "table27"}), clear=True) 87 | def test_wrong_dynamodb_table(dynamodb, lambda_context): 88 | event = sqs_event([{"body": load_event("create_contract_valid_1"), "attributes": {"HttpMethod": "POST"}}]) 89 | 90 | from contracts_service import contract_event_handler # noqa: F401 91 | 92 | create_ddb_table_contracts(dynamodb) 93 | 94 | with pytest.raises(ClientError): 95 | reload(contract_event_handler) 96 | contract_event_handler.lambda_handler(event, lambda_context) 97 | -------------------------------------------------------------------------------- /unicorn_properties/.gitignore: -------------------------------------------------------------------------------- 1 | ### Linux ### 2 | **/*~ 3 | **/.fuse_hidden* 4 | **/.directory 5 | **/.Trash-* 6 | **/.nfs* 7 | 8 | ### OSX ### 9 | **/*.DS_Store 10 | **/.AppleDouble 11 | **/.LSOverride 12 | **/.DocumentRevisions-V100 13 | **/.fseventsd 14 | **/.Spotlight-V100 15 | **/.TemporaryItems 16 | **/.Trashes 17 | **/.VolumeIcon.icns 18 | **/.com.apple.timemachine.donotpresent 19 | 20 | ### JetBrains IDEs ### 21 | **/*.iws 22 | **/.idea/ 23 | **/.idea_modules/ 24 | 25 | ### Python ### 26 | **/__pycache__/ 27 | **/*.py[cod] 28 | **/*$py.class 29 | **/.Python 30 | **/build/ 31 | **/develop-eggs/ 32 | **/dist/ 33 | **/downloads/ 34 | **/eggs/ 35 | **/.eggs/ 36 | **/parts/ 37 | **/sdist/ 38 | **/wheels/ 39 | **/*.egg 40 | **/*.egg-info/ 41 | **/.installed.cfg 42 | **/pip-log.txt 43 | **/pip-delete-this-directory.txt 44 | 45 | ### Unit test / coverage reports ### 46 | **/.cache 47 | **/.coverage 48 | **/.hypothesis/ 49 | **/.pytest_cache/ 50 | **/.tox/ 51 | **/*.cover 52 | **/coverage.xml 53 | **/htmlcov/ 54 | **/nosetests.xml 55 | 56 | ### pyenv ### 57 | **/.python-version 58 | 59 | ### Environments ### 60 | **/.env 61 | **/.venv 62 | **/env/ 63 | **/venv/ 64 | **/ENV/ 65 | **/env.bak/ 66 | **/venv.bak/ 67 | 68 | ### VisualStudioCode ### 69 | **/.vscode/ 70 | **/.history 71 | 72 | ### Windows ### 73 | # Windows thumbnail cache files 74 | **/Thumbs.db 75 | **/ehthumbs.db 76 | **/ehthumbs_vista.db 77 | **/Desktop.ini 78 | **/$RECYCLE.BIN/ 79 | **/*.lnk 80 | 81 | ### requirements.txt ### 82 | # We ignore Python's requirements.txt as we use Poetry instead 83 | **/requirements.txt 84 | **/.aws-sam 85 | -------------------------------------------------------------------------------- /unicorn_properties/Makefile: -------------------------------------------------------------------------------- 1 | #### Global Variables 2 | stackName := $(shell yq -r '.default.global.parameters.stack_name' samconfig.toml) 3 | 4 | 5 | #### Test Variables 6 | 7 | 8 | #### Build/Deploy Tasks 9 | build: 10 | ruff format 11 | sam validate --lint 12 | cfn-lint template.yaml -a cfn_lint_serverless.rules 13 | uv export --no-hashes --format=requirements-txt --output-file=src/requirements.txt 14 | sam build -c $(DOCKER_OPTS) 15 | 16 | deps: 17 | uv sync 18 | 19 | deploy: deps build 20 | sam deploy 21 | 22 | 23 | #### Tests 24 | test: unit-test 25 | 26 | unit-test: 27 | uv run pytest tests/unit/ 28 | 29 | 30 | #### Utilities 31 | sync: 32 | sam sync --stack-name $(stackName) --watch 33 | 34 | logs: 35 | sam logs -t 36 | 37 | clean: 38 | find . -type d -name __pycache__ -exec rm -rf {} \; 2>/dev/null || true 39 | find . -type f -name requirements.txt -exec rm -f {} \; 2>/dev/null || true 40 | rm -rf .pytest_cache/ .aws-sam/ || true 41 | 42 | delete: 43 | sam delete --stack-name $(stackName) --no-prompts 44 | 45 | ci_init: 46 | uv venv 47 | uv export --no-hashes --format=requirements.txt --output-file=src/requirements.txt --extra=dev 48 | uv run pip install -r src/requirements.txt 49 | -------------------------------------------------------------------------------- /unicorn_properties/README.md: -------------------------------------------------------------------------------- 1 | # Developing Unicorn Properties 2 | 3 | ![Properties Approval Architecture](https://static.us-east-1.prod.workshops.aws/public/f273b5fc-17cd-406b-9e63-1d331b00589d/static/images/architecture-properties.png) 4 | 5 | ## Architecture overview 6 | 7 | Unicorn Properties is primarily responsible for approving property listings for Unicorn Web. 8 | 9 | A core component of Unicorn Properties is the approvals workflow. The approvals workflow is implemented using an AWS Step Functions state machine. At a high level, the workflow will: 10 | 11 | * Check whether or not it has any contract information for the property it needs to approve. If there is no contract information, the approval process cannot be completed. 12 | * Ensure the sentiment of the property description is positive and that there no unsafe images. All checks must pass for the listing to be made public. 13 | * Ensure that the contract is in an APPROVED state before it can approve the listing. This accounts for a situation where the property listings are created before the contract has been signed and the services for Unicorn Properties are paid for. 14 | * Publish the result of the workflow via the `PublicationEvaluationCompleted` event. 15 | 16 | The workflow is initiated by a request made by an Unicorn Properties **agent** to have the property approved for publication. Once they have created a property listing (added property details and photos), they initiate the request in Unicorn Web, which generates a `PublicationApprovalRequested` event. This event contains the property information which the workflow processes. 17 | 18 | In order process the approvals workflow successfully, the properties service needs to know the current status of a contract. To remain fully decoupled from the **Contracts Service**, it maintains a local copy of contract status by consuming the `ContractStatusChanged` event. This is eliminates the need for the Contracts service to expose an API that gives other services access to its database, and allows the Properties service to function autonomously. 19 | 20 | When the workflow is paused to check to see whether or not the contract is in an approved state, the `WaitForContractApproval` state will update a contract status for a specified property with its task token. This initiates a stream event on the DynamoDB table. The Property approvals sync function handles DynamoDB stream events. It determines whether or not to pass AWS Step Function task token back to the state machine based on the contract state. 21 | 22 | If workflow is completed successfully, it will emit a `PublicationEvaluationCompleted` event, with an evaluation result of `APPROVED` or `DECLINED`. This is what the Property Web will listen to in order to make the list available for publication. 23 | -------------------------------------------------------------------------------- /unicorn_properties/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/aws-serverless-developer-experience-workshop-python/c3e988cd9a2af13b672c1c79d122568333b4e48c/unicorn_properties/__init__.py -------------------------------------------------------------------------------- /unicorn_properties/integration/PublicationEvaluationCompleted.json: -------------------------------------------------------------------------------- 1 | { 2 | "openapi": "3.0.0", 3 | "info": { 4 | "version": "1.0.0", 5 | "title": "PublicationEvaluationCompleted" 6 | }, 7 | "paths": {}, 8 | "components": { 9 | "schemas": { 10 | "AWSEvent": { 11 | "type": "object", 12 | "required": [ 13 | "detail-type", 14 | "resources", 15 | "detail", 16 | "id", 17 | "source", 18 | "time", 19 | "region", 20 | "version", 21 | "account" 22 | ], 23 | "x-amazon-events-detail-type": "PublicationEvaluationCompleted", 24 | "x-amazon-events-source": "unicorn.web", 25 | "properties": { 26 | "detail": { 27 | "$ref": "#/components/schemas/PublicationEvaluationCompleted" 28 | }, 29 | "account": { 30 | "type": "string" 31 | }, 32 | "detail-type": { 33 | "type": "string" 34 | }, 35 | "id": { 36 | "type": "string" 37 | }, 38 | "region": { 39 | "type": "string" 40 | }, 41 | "resources": { 42 | "type": "array", 43 | "items": { 44 | "type": "string" 45 | } 46 | }, 47 | "source": { 48 | "type": "string" 49 | }, 50 | "time": { 51 | "type": "string", 52 | "format": "date-time" 53 | }, 54 | "version": { 55 | "type": "string" 56 | } 57 | } 58 | }, 59 | "PublicationEvaluationCompleted": { 60 | "type": "object", 61 | "required": [ 62 | "property_id", 63 | "evaluation_result" 64 | ], 65 | "properties": { 66 | "property_id": { 67 | "type": "string" 68 | }, 69 | "evaluation_result": { 70 | "type": "string" 71 | } 72 | } 73 | } 74 | } 75 | } 76 | } -------------------------------------------------------------------------------- /unicorn_properties/integration/subscriber-policies.yaml: -------------------------------------------------------------------------------- 1 | # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | # SPDX-License-Identifier: MIT-0 3 | AWSTemplateFormatVersion: '2010-09-09' 4 | Description: > 5 | Defines the event bus policies that determine who can create rules on the event bus to 6 | subscribe to events published by Unicorn Properties Service. 7 | 8 | Parameters: 9 | Stage: 10 | Type: String 11 | Default: local 12 | AllowedValues: 13 | - local 14 | - dev 15 | - prod 16 | 17 | Resources: 18 | # Update this policy as you get new subscribers by adding their namespace to events:source 19 | CrossServiceCreateRulePolicy: 20 | Type: AWS::Events::EventBusPolicy 21 | Properties: 22 | EventBusName: 23 | Fn::Sub: "{{resolve:ssm:/uni-prop/${Stage}/UnicornPropertiesEventBus}}" 24 | StatementId: 25 | Fn::Sub: "OnlyRulesForPropertiesServiceEvents-${Stage}" 26 | Statement: 27 | Effect: Allow 28 | Principal: 29 | AWS: 30 | Fn::Sub: "arn:${AWS::Partition}:iam::${AWS::AccountId}:root" 31 | Action: 32 | - events:PutRule 33 | - events:DeleteRule 34 | - events:DescribeRule 35 | - events:DisableRule 36 | - events:EnableRule 37 | - events:PutTargets 38 | - events:RemoveTargets 39 | Resource: 40 | - Fn::Sub: 41 | - arn:${AWS::Partition}:events:${AWS::Region}:${AWS::AccountId}:rule/${eventBusName}/* 42 | - eventBusName: 43 | Fn::Sub: "{{resolve:ssm:/uni-prop/${Stage}/UnicornPropertiesEventBus}}" 44 | Condition: 45 | StringEqualsIfExists: 46 | "events:creatorAccount": "${aws:PrincipalAccount}" 47 | StringEquals: 48 | "events:source": 49 | - "{{resolve:ssm:/uni-prop/UnicornPropertiesNamespace}}" 50 | "Null": 51 | "events:source": "false" 52 | -------------------------------------------------------------------------------- /unicorn_properties/integration/subscriptions.yaml: -------------------------------------------------------------------------------- 1 | # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | # SPDX-License-Identifier: MIT-0 3 | AWSTemplateFormatVersion: '2010-09-09' 4 | Description: Defines the rule for the events (subscriptions) that Unicorn Properties wants to consume. 5 | 6 | Parameters: 7 | Stage: 8 | Type: String 9 | Default: local 10 | AllowedValues: 11 | - local 12 | - dev 13 | - prod 14 | 15 | Resources: 16 | #### UNICORN CONTRACTS EVENT SUBSCRIPTIONS 17 | ContractStatusChangedSubscriptionRule: 18 | Type: AWS::Events::Rule 19 | DeletionPolicy: Delete 20 | UpdateReplacePolicy: Delete 21 | Properties: 22 | Name: unicorn.properties-ContractStatusChanged 23 | Description: Contract Status Changed subscription 24 | EventBusName: 25 | Fn::Sub: "{{resolve:ssm:/uni-prop/${Stage}/UnicornContractsEventBusArn}}" 26 | EventPattern: 27 | source: 28 | - "{{resolve:ssm:/uni-prop/UnicornContractsNamespace}}" 29 | detail-type: 30 | - ContractStatusChanged 31 | State: ENABLED 32 | Targets: 33 | - Id: SendEventTo 34 | Arn: 35 | Fn::Sub: "{{resolve:ssm:/uni-prop/${Stage}/UnicornPropertiesEventBusArn}}" 36 | RoleArn: 37 | Fn::GetAtt: [ UnicornPropertiesSubscriptionRole, Arn ] 38 | 39 | #### UNICORN WEB EVENT SUBSCRIPTIONS 40 | PublicationApprovalRequestedSubscriptionRule: 41 | Type: AWS::Events::Rule 42 | DeletionPolicy: Delete 43 | UpdateReplacePolicy: Delete 44 | Properties: 45 | Name: unicorn.properties-PublicationApprovalRequested 46 | Description: Publication evaluation completed subscription 47 | EventBusName: 48 | Fn::Sub: "{{resolve:ssm:/uni-prop/${Stage}/UnicornWebEventBusArn}}" 49 | EventPattern: 50 | source: 51 | - "{{resolve:ssm:/uni-prop/UnicornWebNamespace}}" 52 | detail-type: 53 | - PublicationApprovalRequested 54 | State: ENABLED 55 | Targets: 56 | - Id: SendEventTo 57 | Arn: 58 | Fn::Sub: "{{resolve:ssm:/uni-prop/${Stage}/UnicornPropertiesEventBusArn}}" 59 | RoleArn: 60 | Fn::GetAtt: [ UnicornPropertiesSubscriptionRole, Arn ] 61 | 62 | 63 | # This IAM role allows EventBridge to assume the permissions necessary to send events 64 | # from the publishing event bus, to the subscribing event bus (UnicornPropertiesEventBusArn) 65 | UnicornPropertiesSubscriptionRole: 66 | Type: AWS::IAM::Role 67 | DeletionPolicy: Delete 68 | UpdateReplacePolicy: Delete 69 | Properties: 70 | AssumeRolePolicyDocument: 71 | Statement: 72 | - Effect: Allow 73 | Action: sts:AssumeRole 74 | Principal: 75 | Service: events.amazonaws.com 76 | Policies: 77 | - PolicyName: PutEventsOnUnicornPropertiesEventBus 78 | PolicyDocument: 79 | Version: "2012-10-17" 80 | Statement: 81 | - Effect: Allow 82 | Action: events:PutEvents 83 | Resource: 84 | Fn::Sub: "{{resolve:ssm:/uni-prop/${Stage}/UnicornPropertiesEventBusArn}}" 85 | 86 | Outputs: 87 | ContractStatusChangedSubscription: 88 | Description: Rule ARN for Contract service event subscription 89 | Value: 90 | Fn::GetAtt: [ ContractStatusChangedSubscriptionRule, Arn ] 91 | 92 | PublicationApprovalRequestedSubscription: 93 | Description: Rule ARN for Web service event subscription 94 | Value: 95 | Fn::GetAtt: [ PublicationApprovalRequestedSubscriptionRule, Arn ] 96 | -------------------------------------------------------------------------------- /unicorn_properties/pyproject.toml: -------------------------------------------------------------------------------- 1 | [project] 2 | name = "properties_service" 3 | version = "0.2.0" 4 | description = "Unicorn Properties Property Service" 5 | authors = [ 6 | {name = "Amazon Web Services"} 7 | ] 8 | readme = "README.md" 9 | requires-python = ">=3.12" 10 | 11 | dependencies = [ 12 | "aws-lambda-powertools[tracer]>=3.9.0", 13 | "aws-xray-sdk>=2.14.0", 14 | "boto3>=1.37.23", 15 | ] 16 | 17 | [project.optional-dependencies] 18 | dev = [ 19 | "aws-lambda-powertools[all]>=3.9.0", 20 | "requests>=2.32.3", 21 | "moto[dynamodb,events,sqs]>=5.0.14", 22 | "importlib-metadata>=8.4.0", 23 | "pyyaml>=6.0.2", 24 | "arnparse>=0.0.2", 25 | "pytest>=8.3.4", 26 | "ruff>=0.9.7", 27 | ] 28 | 29 | [tool.setuptools] 30 | package-dir = {"properties_service" = "src"} 31 | packages = ["properties_service"] 32 | -------------------------------------------------------------------------------- /unicorn_properties/pytest.ini: -------------------------------------------------------------------------------- 1 | [pytest] 2 | pythonpath = src 3 | -------------------------------------------------------------------------------- /unicorn_properties/ruff.toml: -------------------------------------------------------------------------------- 1 | # Exclude files/directories from analysis 2 | exclude = [ 3 | ".bzr", 4 | ".direnv", 5 | ".eggs", 6 | ".git", 7 | ".git-rewrite", 8 | ".hg", 9 | ".ipynb_checkpoints", 10 | ".mypy_cache", 11 | ".nox", 12 | ".pants.d", 13 | ".pyenv", 14 | ".pytest_cache", 15 | ".pytype", 16 | ".ruff_cache", 17 | ".svn", 18 | ".tox", 19 | ".venv", 20 | ".vscode", 21 | "__pypackages__", 22 | "_build", 23 | "buck-out", 24 | "build", 25 | "dist", 26 | "node_modules", 27 | "site-packages", 28 | "venv", 29 | ] 30 | 31 | # Same as Black. 32 | line-length = 120 33 | indent-width = 4 34 | 35 | fix = true 36 | 37 | [format] 38 | # Like Black, use double quotes for strings. 39 | quote-style = "double" 40 | 41 | # Like Black, indent with spaces, rather than tabs. 42 | indent-style = "space" 43 | 44 | # Like Black, respect magic trailing commas. 45 | skip-magic-trailing-comma = false 46 | 47 | # Like Black, automatically detect the appropriate line ending. 48 | line-ending = "auto" 49 | 50 | 51 | [lint] 52 | # 1. Enable flake8-bugbear (`B`) rules, in addition to the defaults. 53 | select = ["E4", "E7", "E9", "F", "B"] 54 | 55 | # 2. Avoid enforcing line-length violations (`E501`) 56 | ignore = ["E501"] 57 | 58 | # 3. Avoid trying to fix flake8-bugbear (`B`) violations. 59 | unfixable = ["B"] 60 | 61 | # 4. Ignore `E402` (import violations) in all `__init__.py` files, and in selected subdirectories. 62 | [lint.per-file-ignores] 63 | "__init__.py" = ["E402"] 64 | "**/{tests,docs,tools}/*" = ["E402"] -------------------------------------------------------------------------------- /unicorn_properties/samconfig.toml: -------------------------------------------------------------------------------- 1 | version = 0.1 2 | 3 | [default.global.parameters] 4 | stack_name = "uni-prop-local-properties" 5 | s3_prefix = "uni-prop-local-properties" 6 | resolve_s3 = true 7 | resolve_image_repositories = true 8 | 9 | [default.build.parameters] 10 | cached = true 11 | parallel = true 12 | 13 | [default.deploy.parameters] 14 | disable_rollback = true 15 | confirm_changeset = false 16 | fail_on_empty_changeset = false 17 | capabilities = ["CAPABILITY_IAM", "CAPABILITY_AUTO_EXPAND"] 18 | parameter_overrides = ["Stage=local"] 19 | 20 | [default.validate.parameters] 21 | lint = true 22 | 23 | [default.sync.parameters] 24 | watch = true 25 | 26 | [default.local_start_api.parameters] 27 | warm_containers = "EAGER" 28 | 29 | [default.local_start_lambda.parameters] 30 | warm_containers = "EAGER" -------------------------------------------------------------------------------- /unicorn_properties/src/README.md: -------------------------------------------------------------------------------- 1 | # ContractStatusChanged 2 | 3 | *Automatically generated by the [Amazon Event Schemas](https://aws.amazon.com/)* 4 | 5 | ## Requirements 6 | 7 | 1. Python 36+ 8 | 2. six 1.12.0 9 | 3. regex 2019.11.1 10 | 11 | ## Install Dependencies 12 | ### pip users 13 | 14 | Create and update it in current project's **requirements.txt**: 15 | 16 | ``` 17 | six == 1.12.0 18 | regex == 2019.11.1 19 | ``` 20 | 21 | Run Command: 22 | 23 | ```sh 24 | pip3 install -r requirements.txt 25 | ``` 26 | -------------------------------------------------------------------------------- /unicorn_properties/src/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/aws-serverless-developer-experience-workshop-python/c3e988cd9a2af13b672c1c79d122568333b4e48c/unicorn_properties/src/__init__.py -------------------------------------------------------------------------------- /unicorn_properties/src/properties_service/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/aws-serverless-developer-experience-workshop-python/c3e988cd9a2af13b672c1c79d122568333b4e48c/unicorn_properties/src/properties_service/__init__.py -------------------------------------------------------------------------------- /unicorn_properties/src/properties_service/contract_status_changed_event_handler.py: -------------------------------------------------------------------------------- 1 | # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | # SPDX-License-Identifier: MIT-0 3 | 4 | import os 5 | from datetime import datetime 6 | 7 | import boto3 8 | from aws_lambda_powertools.logging import Logger 9 | from aws_lambda_powertools.metrics import Metrics 10 | from aws_lambda_powertools.tracing import Tracer 11 | from aws_lambda_powertools.event_handler.exceptions import InternalServerError 12 | from schema.unicorn_contracts.contractstatuschanged import AWSEvent, ContractStatusChanged, Marshaller 13 | 14 | # Initialise Environment variables 15 | if (SERVICE_NAMESPACE := os.environ.get("SERVICE_NAMESPACE")) is None: 16 | raise InternalServerError("SERVICE_NAMESPACE environment variable is undefined") 17 | if (CONTRACT_STATUS_TABLE := os.environ.get("CONTRACT_STATUS_TABLE")) is None: 18 | raise InternalServerError("CONTRACT_STATUS_TABLE environment variable is undefined") 19 | 20 | # Initialise PowerTools 21 | logger: Logger = Logger() 22 | tracer: Tracer = Tracer() 23 | metrics: Metrics = Metrics() 24 | 25 | # Initialise boto3 clients 26 | dynamodb = boto3.resource("dynamodb") 27 | table = dynamodb.Table(CONTRACT_STATUS_TABLE) # type: ignore 28 | 29 | # Get current date 30 | now = datetime.now() 31 | current_date = now.strftime("%d/%m/%Y %H:%M:%S") 32 | 33 | 34 | @logger.inject_lambda_context(log_event=True) # type: ignore 35 | @metrics.log_metrics(capture_cold_start_metric=True) # type: ignore 36 | @tracer.capture_method 37 | def lambda_handler(event, context): 38 | """Event handler for ContractStatusChangedEvent 39 | 40 | Parameters 41 | ---------- 42 | event: dict, required 43 | EventBridge Events Format 44 | 45 | context: object, required 46 | Lambda Context runtime methods and attributes 47 | 48 | Context doc: https://docs.aws.amazon.com/lambda/latest/dg/python-context-object.html 49 | 50 | Returns 51 | ------ 52 | The same input event file 53 | """ 54 | # Deserialize event into strongly typed object 55 | awsEvent: AWSEvent = Marshaller.unmarshall(event, AWSEvent) # type: ignore 56 | detail: ContractStatusChanged = awsEvent.detail # type: ignore 57 | 58 | save_contract_status(detail) 59 | 60 | # return OK, async function 61 | return { 62 | "statusCode": 200, 63 | } 64 | 65 | 66 | @tracer.capture_method 67 | def save_contract_status(contract_status_changed_event): 68 | """Saves contract status in contract status table 69 | 70 | Args: 71 | contract_status_changed_event (dict): 72 | Contract_status_changed_event 73 | 74 | Returns: 75 | dict: _description_ 76 | """ 77 | logger.info("Saving contract status to contract status table. %s", contract_status_changed_event.contract_id) 78 | 79 | return table.update_item( 80 | Key={"property_id": contract_status_changed_event.property_id}, 81 | UpdateExpression="set contract_status=:t, contract_last_modified_on=:m, contract_id=:c", 82 | ExpressionAttributeValues={ 83 | ":c": contract_status_changed_event.contract_id, 84 | ":t": contract_status_changed_event.contract_status, 85 | ":m": contract_status_changed_event.contract_last_modified_on, 86 | }, 87 | ) 88 | -------------------------------------------------------------------------------- /unicorn_properties/src/properties_service/exceptions.py: -------------------------------------------------------------------------------- 1 | # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | # SPDX-License-Identifier: MIT-0 3 | 4 | 5 | class ContractStatusNotFoundException(Exception): 6 | """ 7 | Custom exception for encapsulating exceptions Contract Status for a specified property is not found 8 | """ 9 | 10 | def __init__(self, message=None, status_code=None, details=None): 11 | super(ContractStatusNotFoundException, self).__init__() 12 | 13 | self.message = message or "No contract found for specified Property ID" 14 | self.status_code = status_code or 400 15 | self.details = details or {} 16 | -------------------------------------------------------------------------------- /unicorn_properties/src/properties_service/wait_for_contract_approval_function.py: -------------------------------------------------------------------------------- 1 | # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | # SPDX-License-Identifier: MIT-0 3 | 4 | import os 5 | 6 | import boto3 7 | from aws_lambda_powertools.logging import Logger 8 | from aws_lambda_powertools.metrics import Metrics 9 | from aws_lambda_powertools.tracing import Tracer 10 | from aws_lambda_powertools.event_handler.exceptions import InternalServerError 11 | from botocore.exceptions import ClientError 12 | 13 | from properties_service.exceptions import ContractStatusNotFoundException 14 | 15 | 16 | # Initialise Environment variables 17 | if (SERVICE_NAMESPACE := os.environ.get("SERVICE_NAMESPACE")) is None: 18 | raise InternalServerError("SERVICE_NAMESPACE environment variable is undefined") 19 | if (CONTRACT_STATUS_TABLE := os.environ.get("CONTRACT_STATUS_TABLE")) is None: 20 | raise InternalServerError("CONTRACT_STATUS_TABLE environment variable is undefined") 21 | 22 | # Initialise PowerTools 23 | logger: Logger = Logger() 24 | tracer: Tracer = Tracer() 25 | metrics: Metrics = Metrics() 26 | 27 | # Initialise boto3 clients 28 | # sfn = boto3.client('stepfunctions') 29 | dynamodb = boto3.resource("dynamodb") 30 | table = dynamodb.Table(CONTRACT_STATUS_TABLE) # type: ignore 31 | 32 | 33 | @metrics.log_metrics(capture_cold_start_metric=True) # type: ignore 34 | @logger.inject_lambda_context(log_event=True) 35 | @tracer.capture_method 36 | def lambda_handler(event, context): 37 | """Function checks to see whether the contract status exists and waits for APPROVAL 38 | by updating contract status with task token. 39 | 40 | Parameters 41 | ---------- 42 | event: dict, required 43 | Event passed into 44 | 45 | context: object 46 | Lambda Context runtime methods and attributes 47 | 48 | Context doc: https://docs.aws.amazon.com/lambda/latest/dg/python-context-object.html 49 | 50 | Returns 51 | ------ 52 | The same input event file 53 | """ 54 | 55 | task_token: str = event["TaskToken"] 56 | detail: dict = event["Input"] 57 | 58 | try: 59 | contract_status = get_contract_status(detail["property_id"]) 60 | update_token_and_pause_execution(task_token=task_token, property_id=contract_status["property_id"]) 61 | return detail 62 | except ContractStatusNotFoundException as error: 63 | logger.critical("Cannot approve a property that does not exist.") 64 | raise error 65 | 66 | 67 | @tracer.capture_method 68 | def get_contract_status(property_id: str) -> dict: 69 | """Returns contract status for a specified property 70 | 71 | Parameters 72 | ---------- 73 | property_id : str 74 | Property ID 75 | 76 | Returns 77 | ------- 78 | dict 79 | Contract info 80 | """ 81 | 82 | try: 83 | response = table.get_item(Key={"property_id": property_id}) 84 | return response["Item"] 85 | 86 | except ClientError as error: 87 | if error.response["Error"]["Code"] == "ResourceNotFoundException": 88 | logger.exception("Error getting contract.") 89 | raise ContractStatusNotFoundException() from error 90 | raise error 91 | except KeyError as _: 92 | raise ContractStatusNotFoundException() from _ 93 | 94 | 95 | @tracer.capture_method 96 | def update_token_and_pause_execution(task_token: str, property_id: str): 97 | """Update the Contract status table with task token for this state. 98 | 99 | Parameters 100 | ---------- 101 | task_token : str 102 | AWS Step Functions task token 103 | property_id : str 104 | Property ID 105 | """ 106 | table.update_item( 107 | Key={"property_id": property_id}, 108 | UpdateExpression="set sfn_wait_approved_task_token = :g", 109 | ExpressionAttributeValues={":g": task_token}, 110 | ReturnValues="ALL_NEW", 111 | ) 112 | -------------------------------------------------------------------------------- /unicorn_properties/src/schema/unicorn_contracts/contractstatuschanged/ContractStatusChanged.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | import pprint 3 | import re # noqa: F401 4 | 5 | import six 6 | from enum import Enum 7 | 8 | 9 | class ContractStatusChanged(object): 10 | _types = {"contract_id": "str", "contract_last_modified_on": "str", "contract_status": "str", "property_id": "str"} 11 | 12 | _attribute_map = { 13 | "contract_id": "contract_id", 14 | "contract_last_modified_on": "contract_last_modified_on", 15 | "contract_status": "contract_status", 16 | "property_id": "property_id", 17 | } 18 | 19 | def __init__(self, contract_id=None, contract_last_modified_on=None, contract_status=None, property_id=None): # noqa: E501 20 | self._contract_id = None 21 | self._contract_last_modified_on = None 22 | self._contract_status = None 23 | self._property_id = None 24 | self.discriminator = None 25 | self.contract_id = contract_id 26 | self.contract_last_modified_on = contract_last_modified_on 27 | self.contract_status = contract_status 28 | self.property_id = property_id 29 | 30 | @property 31 | def contract_id(self): 32 | return self._contract_id 33 | 34 | @contract_id.setter 35 | def contract_id(self, contract_id): 36 | self._contract_id = contract_id 37 | 38 | @property 39 | def contract_last_modified_on(self): 40 | return self._contract_last_modified_on 41 | 42 | @contract_last_modified_on.setter 43 | def contract_last_modified_on(self, contract_last_modified_on): 44 | self._contract_last_modified_on = contract_last_modified_on 45 | 46 | @property 47 | def contract_status(self): 48 | return self._contract_status 49 | 50 | @contract_status.setter 51 | def contract_status(self, contract_status): 52 | self._contract_status = contract_status 53 | 54 | @property 55 | def property_id(self): 56 | return self._property_id 57 | 58 | @property_id.setter 59 | def property_id(self, property_id): 60 | self._property_id = property_id 61 | 62 | def to_dict(self): 63 | result = {} 64 | 65 | for attr, _ in six.iteritems(self._types): 66 | value = getattr(self, attr) 67 | if isinstance(value, list): 68 | result[attr] = list(map(lambda x: x.to_dict() if hasattr(x, "to_dict") else x, value)) 69 | elif hasattr(value, "to_dict"): 70 | result[attr] = value.to_dict() 71 | elif isinstance(value, dict): 72 | result[attr] = dict( 73 | map( 74 | lambda item: (item[0], item[1].to_dict()) if hasattr(item[1], "to_dict") else item, 75 | value.items(), 76 | ) 77 | ) 78 | else: 79 | result[attr] = value 80 | if issubclass(ContractStatusChanged, dict): 81 | for key, value in self.items(): 82 | result[key] = value 83 | 84 | return result 85 | 86 | def to_str(self): 87 | return pprint.pformat(self.to_dict()) 88 | 89 | def __repr__(self): 90 | return self.to_str() 91 | 92 | def __eq__(self, other): 93 | if not isinstance(other, ContractStatusChanged): 94 | return False 95 | 96 | return self.__dict__ == other.__dict__ 97 | 98 | def __ne__(self, other): 99 | return not self == other 100 | -------------------------------------------------------------------------------- /unicorn_properties/src/schema/unicorn_contracts/contractstatuschanged/__init__.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | 3 | from __future__ import absolute_import 4 | 5 | from schema.unicorn_contracts.contractstatuschanged.marshaller import Marshaller 6 | from schema.unicorn_contracts.contractstatuschanged.AWSEvent import AWSEvent 7 | from schema.unicorn_contracts.contractstatuschanged.ContractStatusChanged import ContractStatusChanged 8 | -------------------------------------------------------------------------------- /unicorn_properties/src/schema/unicorn_web/publicationapprovalrequested/__init__.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | 3 | from __future__ import absolute_import 4 | 5 | from schema.unicorn_properties_web.publicationapprovalrequested.marshaller import Marshaller 6 | from schema.unicorn_properties_web.publicationapprovalrequested.AWSEvent import AWSEvent 7 | from schema.unicorn_properties_web.publicationapprovalrequested.PublicationApprovalRequested import ( 8 | PublicationApprovalRequested, 9 | ) 10 | -------------------------------------------------------------------------------- /unicorn_properties/tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/aws-serverless-developer-experience-workshop-python/c3e988cd9a2af13b672c1c79d122568333b4e48c/unicorn_properties/tests/__init__.py -------------------------------------------------------------------------------- /unicorn_properties/tests/unit/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/aws-serverless-developer-experience-workshop-python/c3e988cd9a2af13b672c1c79d122568333b4e48c/unicorn_properties/tests/unit/__init__.py -------------------------------------------------------------------------------- /unicorn_properties/tests/unit/conftest.py: -------------------------------------------------------------------------------- 1 | # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | # SPDX-License-Identifier: MIT-0 3 | 4 | import os 5 | 6 | import boto3 7 | from aws_lambda_powertools.utilities.typing import LambdaContext 8 | 9 | import pytest 10 | from moto import mock_aws 11 | 12 | 13 | @pytest.fixture(scope="function") 14 | def aws_credentials(): 15 | """Mocked AWS Credentials for moto.""" 16 | os.environ["AWS_ACCESS_KEY_ID"] = "testing" 17 | os.environ["AWS_SECRET_ACCESS_KEY"] = "testing" 18 | os.environ["AWS_SECURITY_TOKEN"] = "testing" 19 | os.environ["AWS_SESSION_TOKEN"] = "testing" 20 | 21 | 22 | @pytest.fixture(scope="function") 23 | def dynamodb(aws_credentials): 24 | with mock_aws(): 25 | yield boto3.resource("dynamodb", region_name="ap-southeast-2") 26 | 27 | 28 | @pytest.fixture(scope="function") 29 | def eventbridge(aws_credentials): 30 | with mock_aws(): 31 | yield boto3.client("events", region_name="ap-southeast-2") 32 | 33 | 34 | @pytest.fixture(scope="function") 35 | def stepfunction(aws_credentials): 36 | with mock_aws(): 37 | yield boto3.client("stepfunctions", region_name="ap-southeast-2") 38 | 39 | 40 | @pytest.fixture(scope="function") 41 | def lambda_context(): 42 | context: LambdaContext = LambdaContext() 43 | context._function_name = "propertiesService-LambdaFunction-HJsvdah2ubi2" 44 | context._function_version = "$LATEST" 45 | context._invoked_function_arn = ( 46 | "arn:aws:lambda:ap-southeast-2:424490683636:function:propertiesService-LambdaFunction-HJsvdah2ubi2" 47 | ) 48 | context._memory_limit_in_mb = 128 49 | context._aws_request_id = "6f970d26-71d6-4c87-a196-9375f85c7b07" 50 | context._log_group_name = "/aws/lambda/propertiesService-LambdaFunction-HJsvdah2ubi2" 51 | context._log_stream_name = "2022/07/14/[$LATEST]7c71ca59882b4c569dd007c7e41c81e8" 52 | # context._identity=CognitoIdentity([cognito_identity_id=None,cognito_identity_pool_id=None])]) 53 | # context._client_context=None 54 | return context 55 | -------------------------------------------------------------------------------- /unicorn_properties/tests/unit/events/ddb_stream_events/contract_status_changed_draft.json: -------------------------------------------------------------------------------- 1 | { 2 | "Records": [ 3 | { 4 | "eventID": "70283d3d4dcfa99d38276979fd1e3f1f", 5 | "eventName": "INSERT", 6 | "eventVersion": "1.1", 7 | "eventSource": "aws:dynamodb", 8 | "awsRegion": "ap-southeast-2", 9 | "dynamodb": { 10 | "ApproximateCreationDateTime": 1661269906.0, 11 | "Keys": { "property_id": { "S": "usa/anytown/main-street/999" } }, 12 | "NewImage": { 13 | "contract_last_modified_on": { "S": "23/08/2022 15:51:44" }, 14 | "contract_id": { "S": "b9c03b51-5b8c-47c8-9d76-223679cde774" }, 15 | "contract_status": { "S": "DRAFT" }, 16 | "property_id": { "S": "usa/anytown/main-street/999" } 17 | }, 18 | "SequenceNumber": "100000000005391461882", 19 | "SizeBytes": 187, 20 | "StreamViewType": "NEW_AND_OLD_IMAGES" 21 | }, 22 | "eventSourceARN": "arn:aws:dynamodb:ap-southeast-2:123456789012:table/aws-unicorn-properties-properties-service-local-ContractStatusTable-GPGCVR510KFA/stream/2022-08-23T15:46:44.107" 23 | } 24 | ] 25 | } 26 | -------------------------------------------------------------------------------- /unicorn_properties/tests/unit/events/ddb_stream_events/sfn_check_exists.json: -------------------------------------------------------------------------------- 1 | { 2 | "Records": [ 3 | { 4 | "eventID": "1dca9e1e9538380e4ef763e3d18bc2e4", 5 | "eventName": "MODIFY", 6 | "eventVersion": "1.1", 7 | "eventSource": "aws:dynamodb", 8 | "awsRegion": "ap-southeast-2", 9 | "dynamodb": { 10 | "ApproximateCreationDateTime": 1661312354.0, 11 | "Keys": { "property_id": { "S": "usa/anytown/main-street/999" } }, 12 | "NewImage": { 13 | "contract_last_modified_on": { "S": "24/08/2022 03:37:28" }, 14 | "contract_id": { "S": "418b32e7-87c4-40d2-a551-4d7a56830905" }, 15 | "contract_status": { "S": "DRAFT" }, 16 | "property_id": { "S": "usa/anytown/main-street/999" }, 17 | "sfn_contract_exists_task_token": { 18 | "S": "AQDIAAAAKgAAAAMAAAAAAAAAASxHOzYwUsA8mg9FgpS7TiMpXEm01Dro6ACdTxRxl9d8dCtnuHT1deXjaAkkzwh2ytaUpAjwWWcGgjA/pQUbzhdSBhE/oRx7ASRUNd4weldA53bOnM4TNL4y/k6nfPAsrnFiL++wyk6ERt/VSUsquBM6Mm1Kw+/9r6QqmmVkWoBfg/+KVA==sWAo4hixm0jircgkrfnKH5o4lPGXsHZz1AuALF0Lvy70waNMfwgmCz0PXejS+PcXEHLJ0eA9/IuBlhOfUQeHzv5hdMj9jh1n62k387CytHq5LPPTuqsLHizplCsapv3HYiHb8CZmeEHNVD0jBOZvUPx28v1ERDp4UkVAm1Kilxyt30ORmMZwZ7A9ucMRpMxjmK0uPUjP35yvbXU/z9R9sYAAK/e01mTnl1O5G5v7Fy/0qpGFYlMbONcl/+DWKwjysYLs33/CSuRaKQFsd5a9LTH5VrAM7lzvHNvbnb+ZeAIfMWz7clVJyo1f4kaqpHafQf+4B3TQOdDsb/ASpu2QYsw6o24PMUoaZQ67fST/tAEsAHm1h41L0YhWwpC/dWlHReUABQ3PKd+wT1VPs3wpW/PzwRddwzB6laFvN38YU5sTU7widqGY14OvN5W9vzb4iaFtdUmbfMWxinML0sRBSFUQHWmCYDnOliIR36sq4T6GkxuNVue7RWlhDbDLQu9SlM00nSxg/9dbcl4wAwxH" 19 | } 20 | }, 21 | "OldImage": { 22 | "contract_last_modified_on": { "S": "24/08/2022 03:37:28" }, 23 | "contract_id": { "S": "418b32e7-87c4-40d2-a551-4d7a56830905" }, 24 | "contract_status": { "S": "DRAFT" }, 25 | "property_id": { "S": "usa/anytown/main-street/999" } 26 | }, 27 | "SequenceNumber": "2360500000000006677219691", 28 | "SizeBytes": 1102, 29 | "StreamViewType": "NEW_AND_OLD_IMAGES" 30 | }, 31 | "eventSourceARN": "arn:aws:dynamodb:ap-southeast-2:123456789012:table/aws-unicorn-properties-properties-service-local-ContractStatusTable-GPGCVR510KFA/stream/2022-08-23T15:46:44.107" 32 | } 33 | ] 34 | } 35 | -------------------------------------------------------------------------------- /unicorn_properties/tests/unit/events/ddb_stream_events/sfn_wait_approval.json: -------------------------------------------------------------------------------- 1 | { 2 | "Records": [ 3 | { 4 | "eventID": "eb388b2e39dc9da93600f611374f588b", 5 | "eventName": "MODIFY", 6 | "eventVersion": "1.1", 7 | "eventSource": "aws:dynamodb", 8 | "awsRegion": "ap-southeast-2", 9 | "dynamodb": { 10 | "ApproximateCreationDateTime": 1661345617.0, 11 | "Keys": { "property_id": { "S": "usa/anytown/main-street/999" } }, 12 | "NewImage": { 13 | "contract_last_modified_on": { "S": "24/08/2022 08:09:11" }, 14 | "contract_id": { "S": "3a574672-a87f-4150-8286-506cfeea4913" }, 15 | "contract_status": { "S": "DRAFT" }, 16 | "sfn_verify_approved_task_token": { 17 | "S": "AQDIAAAAKgAAAAMAAAAAAAAAATHiOzW3P9kS+UUCeOLc19jKIJ1NMlo4NoiGqkf5yt+P6Wv6DNHN71Sxsq0MSHeOfmjJEKHgVOgs55fI/cgvjAeesz/iWJvOrv7va2bHQuRMgSFT1J8HfMsA8O4TRTyv/yt14N+w0rSBuJLgJjOil23JGXjoh/989KvkDwwIIRVJJ6uHEw==/hCsUZVkaUn3WfnF4amPTo/9lWL3cG3HiDDFQgMH3owU0HbDV3vd3IlxMK0z73Y9D9oojAfwO1GrxPfPq/THtQvX15dlIOvEyPyiKwNo0y2kxuwoJDrwWpY/4NNbNEwOGIagivrQG2GURD89jLtf7FQUrryDIMadvx3bpfXIzRj5hRjArTR7sEhOWfiV1JPT5ReJvkxi++W3I9zAY+cdh4DYtnfZd2wXP5Di1/VBgz+TOviQEeeZFeg8tEM3S/xDpiRdSWCSiKFeY2I6jFgFzywhrCrDMWkt08eB1aZhxQuvMDbt4+3vBeIYoYhozOPI0Q8JAfOhIJ3EewNRFXbnrhGPDoctnE2tMQ5y8n8xTrp/DGRSEZ3IDRyxI6w1j8oyCsbqaEAB0d8jpXOaM4yuxaijbzU8czw6JjA6c+N2DIl3eHsl5OUcat/+298HdoFUo9BiICbEowAZePNh2uu/8CkAqam3h68JydNW6nZA3fo6agSBL9IFe4xGFU79Lu8AMPhIehKWuWPVeRsnqaUx" 18 | }, 19 | "property_id": { "S": "usa/anytown/main-street/999" }, 20 | "sfn_contract_exists_task_token": { 21 | "S": "AQDIAAAAKgAAAAMAAAAAAAAAAVS9BUaG0YfI6XbiFBynBEemcfQshPfHZqpHkUSL5AI0H54qZ95l2VRQ17BIZuhBNSfJyNLryGJFYq3Ro80CJcn5NePgWmpZt6Tx0SxZOtTaqU9Ozksy3p56Kg0uMABO1vmm5WJj7mnaaFmGwCl10+40Mrnmhqh6Q49BqcTZ1Ud92+NrPA==Z7mKaWIDnW9z8IviYx1YsRNkwpmKHwECnyFLgODmLEnwv9HKJCdKZV27GdM/A+C7X2OYyCei4fp6VG1l4qMVwukmt6nTc7vdTBxCV32C+mGSmhOWiK/qxRTxRKukLmrf5ahlgE/JDj8vHhptOVyqXNPzh3cYDURW/WMNR6EP+k9s1vsPvgOOivwMYkVAN0+Xpu4HV9gdSw0OqDAHScZUjVfvXxPVSnUrrvEpeUUflAUafO2Z+mdzCl173qTzd0povZXdkTKyMmdlILI6sZhuahnkuDcGD6Pnoq7XL/i3bDDGR90LCqxWnTorkec0HBNVct4/RltQnvRY0zASG6N9LDpXOy0qLY9ypgbWU4vEnTxOP0j3QYJjA8wO2wih45T3JRKGZ4N6U22WOEUwhePCyvr6oDBDel0cOS7nZE1fP3gn7k42ssnxIBGGzIsXp39HbnDkp8b4a4MNHmZ6tPcySyx9A5bokRG3XAaZfHZgqcUTSyCRCcbOxANkPxSvJADVHC1by8mp/j6tw202kXBD" 22 | } 23 | }, 24 | "OldImage": { 25 | "contract_last_modified_on": { "S": "24/08/2022 08:09:11" }, 26 | "contract_id": { "S": "3a574672-a87f-4150-8286-506cfeea4913" }, 27 | "contract_status": { "S": "DRAFT" }, 28 | "property_id": { "S": "usa/anytown/main-street/999" }, 29 | "sfn_contract_exists_task_token": { 30 | "S": "AQDIAAAAKgAAAAMAAAAAAAAAAVS9BUaG0YfI6XbiFBynBEemcfQshPfHZqpHkUSL5AI0H54qZ95l2VRQ17BIZuhBNSfJyNLryGJFYq3Ro80CJcn5NePgWmpZt6Tx0SxZOtTaqU9Ozksy3p56Kg0uMABO1vmm5WJj7mnaaFmGwCl10+40Mrnmhqh6Q49BqcTZ1Ud92+NrPA==Z7mKaWIDnW9z8IviYx1YsRNkwpmKHwECnyFLgODmLEnwv9HKJCdKZV27GdM/A+C7X2OYyCei4fp6VG1l4qMVwukmt6nTc7vdTBxCV32C+mGSmhOWiK/qxRTxRKukLmrf5ahlgE/JDj8vHhptOVyqXNPzh3cYDURW/WMNR6EP+k9s1vsPvgOOivwMYkVAN0+Xpu4HV9gdSw0OqDAHScZUjVfvXxPVSnUrrvEpeUUflAUafO2Z+mdzCl173qTzd0povZXdkTKyMmdlILI6sZhuahnkuDcGD6Pnoq7XL/i3bDDGR90LCqxWnTorkec0HBNVct4/RltQnvRY0zASG6N9LDpXOy0qLY9ypgbWU4vEnTxOP0j3QYJjA8wO2wih45T3JRKGZ4N6U22WOEUwhePCyvr6oDBDel0cOS7nZE1fP3gn7k42ssnxIBGGzIsXp39HbnDkp8b4a4MNHmZ6tPcySyx9A5bokRG3XAaZfHZgqcUTSyCRCcbOxANkPxSvJADVHC1by8mp/j6tw202kXBD" 31 | } 32 | }, 33 | "SequenceNumber": "4068300000000009582749593", 34 | "SizeBytes": 2634, 35 | "StreamViewType": "NEW_AND_OLD_IMAGES" 36 | }, 37 | "eventSourceARN": "arn:aws:dynamodb:ap-southeast-2:123456789012:table/aws-unicorn-properties-properties-service-local-ContractStatusTable-GPGCVR510KFA/stream/2022-08-23T15:46:44.107" 38 | } 39 | ] 40 | } 41 | -------------------------------------------------------------------------------- /unicorn_properties/tests/unit/events/ddb_stream_events/status_approved_waiting_for_approval.json: -------------------------------------------------------------------------------- 1 | { 2 | "Records": [ 3 | { 4 | "eventID": "ce16edd66e3812941e0d721e877c752a", 5 | "eventName": "MODIFY", 6 | "eventVersion": "1.1", 7 | "eventSource": "aws:dynamodb", 8 | "awsRegion": "ap-southeast-2", 9 | "dynamodb": { 10 | "ApproximateCreationDateTime": 1661391844, 11 | "Keys": { 12 | "property_id": { 13 | "S": "usa/anytown/main-street/111" 14 | } 15 | }, 16 | "NewImage": { 17 | "contract_last_modified_on": { 18 | "S": "25/08/2022 01:44:02" 19 | }, 20 | "contract_id": { 21 | "S": "ef3f01a5-79ec-411a-b59a-7dbb19208ead" 22 | }, 23 | "contract_status": { 24 | "S": "APPROVED" 25 | }, 26 | "property_id": { 27 | "S": "usa/anytown/main-street/111" 28 | } 29 | }, 30 | "OldImage": { 31 | "contract_last_modified_on": { 32 | "S": "24/08/2022 15:53:26" 33 | }, 34 | "sfn_wait_approved_task_token": { 35 | "S": "AQDIAAAAKgAAAAMAAAAAAAAAAapnwdMR3Z7RAg3IavSq2hbHt+CZPIQYaakFO6Em9Ik00VsGcaznxotIUGB2t7kihvuu/ffeoF+Z4yg4dggTxtzVwvRJwUDQr33/s/LhJyvEfNS57PXCv/CYFssJZ+28FRCAYbGekKaopYhaUlvq0taLGMaEfIbRBeLUmLHInDkPPDbwTA==N0LRrCP3bIFW1MPkMQC3kd35p8yflvpThHCeviqe3qeyKBX03+ziocuyvNHVpktMuECnHL3MN9a6BfpSM1KItYI+qdIC74ls83ALSjjOs8G8pOz4OudcliLYAvmZPRQXvFaw6aSMLfJJ7xRpEFSBwj1zDzbadMLtfudG78pmZX1m75/idMU5gz0UPkC87bVQN6Kyjl7obxAeUO4aoqGJeNz6WbJQtrsUhiQWVEH/AwjUAj9Q0DwRqRPqeWyrv4MIMKea/Xu9vhXbcS+zPWPq8onN9fyAqoMNh64K6wSGWxtAbaaByKxNpQu7o9ho/Iu/ME0KOAqyUK6vcnXOpIwIoMAZiG34KF4UnQsD0gIokQcGLbehMGRixvEJMDZIloLxkuH0jvpIvD5xokGxpHwiVMISi2XRJ92nnGmWTCLWqsqJlsg4We6snJp0Akw2w1Nt41lgY8kWkjxHNWEHDIMjzx1zeWiVa9b9aDDcckb71ouknJCN4gbxVs+yP30M97qnCEmMrc25Yq7cEXLhu5Dh" 36 | }, 37 | "contract_id": { 38 | "S": "ef3f01a5-79ec-411a-b59a-7dbb19208ead" 39 | }, 40 | "contract_status": { 41 | "S": "DRAFT" 42 | }, 43 | "property_id": { 44 | "S": "usa/anytown/main-street/111" 45 | }, 46 | "sfn_contract_exists_task_token": { 47 | "S": "AQDIAAAAKgAAAAMAAAAAAAAAAfL0WOHwLPeUsqktBZSKrsN4dJqIHsLoClU1UUkV+fstVYTXYkMNsmLNyfmrWPVOpQrtK0JKSw+SmuA0qq5qIx/bwz10JfWY7EUB8BHSW6AeYPdzOwHUDqSLs+OzCBl4HhqbeLWCadXOrXRylY5N6P0QOmjLUN/08v+8Ioz2DvKqa3pUbQ==InWjQiaSHWHtJgpe+1IxIipKGyk0vg+LqvlIKRiJENbE1Hz2zZK3aROztTCnHISR2rYBjTDihcn3S6QTqRrlhLvlkxBrxIIzz8UB4v4PaPLklUFPV8IEF0JpT4uhpRST2QV7RuHBZWgQOcMRpM+0yvHZ5Pq4YNzJHKpkvEW34Kv4wsduWbYG3Wl6Yej5TvAHNpoqi3PS+Z2/cih/EDQmTiznmwLPptnA0wc1RrhShT7Izm2hR/lINW9LcY9346oyxEK2riWm9v9kxMB8vg926nqdIKwwDNCsMrpQGgOsIfW+ECQNPTefuPWIwZ4ycvzWeEP6mdhZ+pusD4+XUDIZ5Jjok74YIxqHE2nSck+kLyqpXxcxBc6CRX+SUSCUuWyWEpjYx43qAfTRgLhm1r+DlI5sg1ARHe0oVUoeuvV7jX2b9AHarMEiPUxvTGqn/lRM9c8x55TskXxF9EE7AV0hCzCC+DiT3nj70LZ/7iSJZpyORH3B3KEEJQA/7BtE4LYPVWmIhbVjXr0mf67zVl6Y" 48 | } 49 | }, 50 | "SequenceNumber": "6547000000000005631781392", 51 | "SizeBytes": 1869, 52 | "StreamViewType": "NEW_AND_OLD_IMAGES" 53 | }, 54 | "eventSourceARN": "arn:aws:dynamodb:ap-southeast-2:123456789012:table/aws-unicorn-properties-properties-service-local-ContractStatusTable-GPGCVR510KFA/stream/2022-08-23T15:46:44.107" 55 | } 56 | ] 57 | } 58 | -------------------------------------------------------------------------------- /unicorn_properties/tests/unit/events/ddb_stream_events/status_approved_with_no_workflow.json: -------------------------------------------------------------------------------- 1 | { 2 | "Records": [ 3 | { 4 | "eventID": "8178a46079764b693e00ef96c1f6cfa6", 5 | "eventName": "MODIFY", 6 | "eventVersion": "1.1", 7 | "eventSource": "aws:dynamodb", 8 | "awsRegion": "ap-southeast-2", 9 | "dynamodb": { 10 | "ApproximateCreationDateTime": 1661270661.0, 11 | "Keys": { "property_id": { "S": "usa/anytown/main-street/999" } }, 12 | "NewImage": { 13 | "contract_last_modified_on": { "S": "23/08/2022 16:04:19" }, 14 | "contract_id": { "S": "b9c03b51-5b8c-47c8-9d76-223679cde774" }, 15 | "contract_status": { "S": "APPROVED" }, 16 | "property_id": { "S": "usa/anytown/main-street/999" } 17 | }, 18 | "OldImage": { 19 | "contract_last_modified_on": { "S": "23/08/2022 15:51:44" }, 20 | "contract_id": { "S": "b9c03b51-5b8c-47c8-9d76-223679cde774" }, 21 | "contract_status": { "S": "DRAFT" }, 22 | "property_id": { "S": "usa/anytown/main-street/999" } 23 | }, 24 | "SequenceNumber": "200000000005392276079", 25 | "SizeBytes": 339, 26 | "StreamViewType": "NEW_AND_OLD_IMAGES" 27 | }, 28 | "eventSourceARN": "arn:aws:dynamodb:ap-southeast-2:123456789012:table/aws-unicorn-properties-properties-service-local-ContractStatusTable-GPGCVR510KFA/stream/2022-08-23T15:46:44.107" 29 | } 30 | ] 31 | } 32 | -------------------------------------------------------------------------------- /unicorn_properties/tests/unit/events/eventbridge/contract_status_changed.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0", 3 | "account": "123456789012", 4 | "region": "us-east-1", 5 | "detail-type": "ContractStatusChanged", 6 | "source": "unicorn.contracts", 7 | "time": "2022-08-14T22:06:31Z", 8 | "id": "c071bfbf-83c4-49ca-a6ff-3df053957145", 9 | "resources": [], 10 | "detail": { 11 | "contract_updated_on": "10/08/2022 19:56:30", 12 | "contract_id": "222", 13 | "property_id": "usa/anytown/main-street/123", 14 | "contract_status": "DRAFT" 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /unicorn_properties/tests/unit/events/eventbridge/contract_status_changed_event_contract_1_approved.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "DetailType": "ContractStatusChanged", 4 | "Source": "unicorn.contracts", 5 | "EventBusName": "UnicornPropertiesBus-local", 6 | "Detail": "{ \"contract_updated_on\": \"10/08/2022 19:56:30\", \"contract_id\": \"f2bedc80-3dc8-4544-9140-9b606d71a6ee\", \"property_id\": \"usa/anytown/main-street/111\", \"contract_status\": \"APPROVED\" }" 7 | } 8 | ] 9 | -------------------------------------------------------------------------------- /unicorn_properties/tests/unit/events/eventbridge/contract_status_changed_event_contract_1_draft.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "DetailType": "ContractStatusChanged", 4 | "Source": "unicorn.contracts", 5 | "EventBusName": "UnicornPropertiesBus-local", 6 | "Detail": "{ \"contract_updated_on\": \"10/08/2022 19:56:30\", \"contract_id\": \"f2bedc80-3dc8-4544-9140-9b606d71a6ee\", \"property_id\": \"usa/anytown/main-street/111\", \"contract_status\": \"DRAFT\" }" 7 | } 8 | ] 9 | -------------------------------------------------------------------------------- /unicorn_properties/tests/unit/events/eventbridge/contract_status_changed_event_contract_2_approved.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "DetailType": "ContractStatusChanged", 4 | "Source": "unicorn.contracts", 5 | "EventBusName": "UnicornPropertiesBus-local", 6 | "Detail": "{ \"contract_updated_on\": \"10/08/2022 19:56:30\", \"contract_id\": \"9183453b-d284-4466-a2d9-f00b1d569ad7\", \"property_id\": \"usa/anytown/main-street/222\", \"contract_status\": \"APPROVED\" }" 7 | } 8 | ] 9 | -------------------------------------------------------------------------------- /unicorn_properties/tests/unit/events/eventbridge/contract_status_changed_event_contract_2_draft.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "DetailType": "ContractStatusChanged", 4 | "Source": "unicorn.contracts", 5 | "EventBusName": "UnicornPropertiesBus-local", 6 | "Detail": "{ \"contract_updated_on\": \"10/08/2022 19:56:30\", \"contract_id\": \"9183453b-d284-4466-a2d9-f00b1d569ad7\", \"property_id\": \"usa/anytown/main-street/222\", \"contract_status\": \"DRAFT\" }" 7 | } 8 | ] 9 | -------------------------------------------------------------------------------- /unicorn_properties/tests/unit/events/eventbridge/publication_approval_requested_event.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0", 3 | "id": "f849f683-76e1-1c84-669d-544a9828dfef", 4 | "detail-type": "PublicationApprovalRequested", 5 | "source": "unicorn.web", 6 | "account": "123456789012", 7 | "time": "2022-08-16T06:33:05Z", 8 | "region": "ap-southeast-2", 9 | "resources": [], 10 | "detail": { 11 | "property_id": "usa/anytown/main-street/111", 12 | "address": { 13 | "country": "USA", 14 | "city": "Anytown", 15 | "street": "Main Street", 16 | "number": 111 17 | }, 18 | "description": "This classic Anytown estate comes with a covetable lake view. The romantic and comfortable backyard is the perfect setting for unicorn get-togethers. The open concept Main Stable is fully equipped with all the desired amenities. Second floor features 6 straw bales including large Rainbow Suite with private training pool terrace and Jr Sparkles Suite.", 19 | "contract": "sale", 20 | "listprice": 200, 21 | "currency": "SPL", 22 | "images": [ 23 | "property_images/prop1_exterior1.jpg", 24 | "property_images/prop1_interior1.jpg", 25 | "property_images/prop1_interior2.jpg", 26 | "property_images/prop1_interior3.jpg", 27 | "property_images/prop1_interior4-bad.jpg" 28 | ] 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /unicorn_properties/tests/unit/events/eventbridge/publication_approval_requested_event_all_good.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "DetailType": "PublicationApprovalRequested", 4 | "Source": "unicorn.web", 5 | "EventBusName": "UnicornPropertiesBus-local", 6 | "Detail": "{\"property_id\":\"usa/anytown/main-street/222\",\"address\":{\"country\":\"USA\",\"city\":\"Anytown\",\"street\":\"Main Street\",\"number\":222},\"description\":\"This classic Anytown estate comes with a covetable lake view. The romantic and comfortable backyard is the perfect setting for unicorn get-togethers. The open concept Main Stable is fully equipped with all the desired amenities. Second floor features 6 straw bales including large Rainbow Suite with private training pool terrace and Jr Sparkles Suite.\",\"contract\":\"sale\",\"listprice\":200,\"currency\":\"SPL\",\"images\":[\"property_images/prop1_exterior1.jpg\",\"property_images/prop1_interior1.jpg\",\"property_images/prop1_interior2.jpg\",\"property_images/prop1_interior3.jpg\"]}" 7 | } 8 | ] 9 | -------------------------------------------------------------------------------- /unicorn_properties/tests/unit/events/eventbridge/publication_approval_requested_event_inappropriate_description.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "DetailType": "PublicationApprovalRequested", 4 | "Source": "unicorn.web", 5 | "EventBusName": "UnicornPropertiesBus-local", 6 | "Detail": "{\"property_id\":\"usa/anytown/main-street/111\",\"address\":{\"country\":\"USA\",\"city\":\"Anytown\",\"street\":\"Main Street\",\"number\":111},\"description\":\"This is a property for goblins. The property has the worst quality and is atrocious when it comes to design. The property is not clean whatsoever, and will make any property owner have buyers' remorse as soon the property is bought. Keep away from this property as much as possible!\",\"contract\":\"sale\",\"listprice\":200,\"currency\":\"SPL\",\"images\":[\"property_images/prop1_exterior1.jpg\",\"property_images/prop1_interior1.jpg\",\"property_images/prop1_interior2.jpg\",\"property_images/prop1_interior3.jpg\"]}" 7 | } 8 | ] 9 | -------------------------------------------------------------------------------- /unicorn_properties/tests/unit/events/eventbridge/publication_approval_requested_event_inappropriate_images.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "DetailType": "PublicationApprovalRequested", 4 | "Source": "unicorn.web", 5 | "EventBusName": "UnicornPropertiesBus-local", 6 | "Detail": "{\"property_id\":\"usa/anytown/main-street/111\",\"address\":{\"country\":\"USA\",\"city\":\"Anytown\",\"street\":\"Main Street\",\"number\":111},\"description\":\"This classic Anytown estate comes with a covetable lake view. The romantic and comfortable backyard is the perfect setting for unicorn get-togethers. The open concept Main Stable is fully equipped with all the desired amenities. Second floor features 6 straw bales including large Rainbow Suite with private training pool terrace and Jr Sparkles Suite.\",\"contract\":\"sale\",\"listprice\":200,\"currency\":\"SPL\",\"images\":[\"property_images/prop1_exterior1.jpg\",\"property_images/prop1_interior1.jpg\",\"property_images/prop1_interior2.jpg\",\"property_images/prop1_interior3.jpg\",\"property_images/prop1_interior4-bad.jpg\"]}" 7 | } 8 | ] 9 | -------------------------------------------------------------------------------- /unicorn_properties/tests/unit/events/eventbridge/publication_approval_requested_event_non_existing_contract.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "DetailType": "PublicationApprovalRequested", 4 | "Source": "unicorn.web", 5 | "EventBusName": "UnicornPropertiesBus-local", 6 | "Detail": "{\"property_id\":\"usa/anytown/main-street/333\",\"address\":{\"country\":\"USA\",\"city\":\"Anytown\",\"street\":\"Main Street\",\"number\":333},\"description\":\"This classic Anytown estate comes with a covetable lake view. The romantic and comfortable backyard is the perfect setting for unicorn get-togethers. The open concept Main Stable is fully equipped with all the desired amenities. Second floor features 6 straw bales including large Rainbow Suite with private training pool terrace and Jr Sparkles Suite.\",\"contract\":\"sale\",\"listprice\":200,\"currency\":\"SPL\",\"images\":[\"property_images/prop1_exterior1.jpg\",\"property_images/prop1_interior1.jpg\",\"property_images/prop1_interior2.jpg\",\"property_images/prop1_interior3.jpg\"]}" 7 | } 8 | ] 9 | -------------------------------------------------------------------------------- /unicorn_properties/tests/unit/events/eventbridge/publication_approval_requested_event_pause_workflow.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "DetailType": "PublicationApprovalRequested", 4 | "Source": "unicorn.web", 5 | "EventBusName": "UnicornPropertiesBus-local", 6 | "Detail": "{\"property_id\":\"usa/anytown/main-street/111\",\"address\":{\"country\":\"USA\",\"city\":\"Anytown\",\"street\":\"Main Street\",\"number\":111},\"description\":\"This classic Anytown estate comes with a covetable lake view. The romantic and comfortable backyard is the perfect setting for unicorn get-togethers. The open concept Main Stable is fully equipped with all the desired amenities. Second floor features 6 straw bales including large Rainbow Suite with private training pool terrace and Jr Sparkles Suite.\",\"contract\":\"sale\",\"listprice\":200,\"currency\":\"SPL\",\"images\":[\"property_images/prop1_exterior1.jpg\",\"property_images/prop1_interior1.jpg\",\"property_images/prop1_interior2.jpg\",\"property_images/prop1_interior3.jpg\"]}" 7 | } 8 | ] 9 | -------------------------------------------------------------------------------- /unicorn_properties/tests/unit/events/eventbridge/publication_evaluation_completed_event.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0", 3 | "id": "f849f683-76e1-1c84-669d-544a9828dfef", 4 | "detail-type": "PublicationEvaluationCompleted", 5 | "source": "unicorn.properties", 6 | "account": "123456789012", 7 | "time": "2022-08-16T06:33:05Z", 8 | "region": "ap-southeast-2", 9 | "resources": [], 10 | "detail": { 11 | "property_id": "usa/anytown/main-street/111", 12 | "evaluation_result": "APPROVED|DECLINED", 13 | "result_reason": "UNSAFE_IMAGE_DETECTED|BAD_SENTIMENT_DETECTED|..." 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /unicorn_properties/tests/unit/events/eventbridge/put_event_property_approval_requested.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "Source": "unicorn.web", 4 | "Detail": "{ \"property_id\": \"usa/anytown/main-street/111\", \"country\": \"USA\", \"city\": \"Anytown\", \"street\": \"Main Street\", \"number\": 111, \"description\": \"This classic Anytown estate comes with a covetable lake view. The romantic and comfortable backyard is the perfect setting for unicorn get-togethers. The open concept Main Stable is fully equipped with all the desired amenities. Second floor features 6 straw bales including large Rainbow Suite with private training pool terrace and Jr Sparkles Suite.\", \"contract\": \"sale\", \"listprice\": 200, \"currency\": \"SPL\", \"images\": [ \"prop1_exterior1.jpg\", \"prop1_interior1.jpg\", \"prop1_interior2.jpg\", \"prop1_interior3.jpg\", \"prop1_interior4-bad.jpg\" ] }", 5 | "DetailType": "PublicationApprovalRequested", 6 | "EventBusName": "UnicornPropertiesBus-local" 7 | } 8 | ] 9 | -------------------------------------------------------------------------------- /unicorn_properties/tests/unit/events/lambda/content_integrity_validator_function_success.json: -------------------------------------------------------------------------------- 1 | { 2 | "imageModerations": [ 3 | { 4 | "ModerationLabels": [] 5 | } 6 | ], 7 | "contentSentiment": { 8 | "Sentiment": "POSITIVE" 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /unicorn_properties/tests/unit/events/lambda/contract_status_checker.json: -------------------------------------------------------------------------------- 1 | { 2 | "Input": { 3 | "property_id": "usa/anytown/main-street/123", 4 | "country": "USA", 5 | "city": "Anytown", 6 | "street": "Main Street", 7 | "number": 123, 8 | "description": "This classic Anytown estate comes with a covetable lake view. The romantic and comfortable backyard is the perfect setting for unicorn get-togethers. The open concept Main Stable is fully equipped with all the desired amenities. Second floor features 6 straw bales including large Rainbow Suite with private training pool terrace and Jr Sparkles Suite.", 9 | "contract": "sale", 10 | "listprice": 200.0, 11 | "currency": "SPL", 12 | "images": [ 13 | "usa/anytown/main-street-123-0d61b4e3" 14 | ] 15 | } 16 | } -------------------------------------------------------------------------------- /unicorn_properties/tests/unit/events/lambda/wait_for_contract_approval_function.json: -------------------------------------------------------------------------------- 1 | { 2 | "TaskToken": "xxx", 3 | "Input": { 4 | "property_id": "usa/anytown/main-street/123" 5 | } 6 | } -------------------------------------------------------------------------------- /unicorn_properties/tests/unit/helper.py: -------------------------------------------------------------------------------- 1 | # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | # SPDX-License-Identifier: MIT-0 3 | import json 4 | from pathlib import Path 5 | 6 | 7 | TABLE_NAME = "table1" 8 | EVENTBUS_NAME = "test-eventbridge" 9 | EVENTS_DIR = Path(__file__).parent / "events" 10 | 11 | 12 | def load_event(filename): 13 | return json.load(open(EVENTS_DIR / f"{filename}.json", "r")) 14 | 15 | 16 | def return_env_vars_dict(k={}): 17 | d = { 18 | "AWS_DEFAULT_REGION": "ap-southeast-2", 19 | "CONTRACT_STATUS_TABLE": TABLE_NAME, 20 | "EVENT_BUS": EVENTBUS_NAME, 21 | "SERVICE_NAMESPACE": "unicorn.properties", 22 | "POWERTOOLS_SERVICE_NAME": "unicorn.properties", 23 | "POWERTOOLS_TRACE_DISABLED": "true", 24 | "POWERTOOLS_LOGGER_LOG_EVENT": "true", 25 | "POWERTOOLS_LOGGER_SAMPLE_RATE": "0.1", 26 | "POWERTOOLS_METRICS_NAMESPACE": "unicorn.properties", 27 | "LOG_LEVEL": "INFO", 28 | } 29 | d.update(k) 30 | return d 31 | 32 | 33 | def create_ddb_table_properties(dynamodb): 34 | table = dynamodb.create_table( 35 | TableName=TABLE_NAME, 36 | KeySchema=[{"AttributeName": "property_id", "KeyType": "HASH"}], 37 | AttributeDefinitions=[{"AttributeName": "property_id", "AttributeType": "S"}], 38 | ProvisionedThroughput={ 39 | "ReadCapacityUnits": 1, 40 | "WriteCapacityUnits": 1, 41 | }, 42 | ) 43 | table.meta.client.get_waiter("table_exists").wait(TableName=TABLE_NAME) 44 | return table 45 | 46 | 47 | def create_ddb_table_contracts_with_entry(dynamodb): 48 | table = dynamodb.create_table( 49 | TableName=TABLE_NAME, 50 | KeySchema=[{"AttributeName": "property_id", "KeyType": "HASH"}], 51 | AttributeDefinitions=[ 52 | {"AttributeName": "property_id", "AttributeType": "S"}, 53 | ], 54 | ProvisionedThroughput={"ReadCapacityUnits": 1, "WriteCapacityUnits": 1}, 55 | ) 56 | table.meta.client.get_waiter("table_exists").wait(TableName=TABLE_NAME) 57 | contract = { 58 | "property_id": "usa/anytown/main-street/123", # PK 59 | "contact_created": "01/08/2022 20:36:30", 60 | "contract_last_modified_on": "01/08/2022 20:36:30", 61 | "contract_id": "11111111", 62 | "address": {"country": "USA", "city": "Anytown", "street": "Main Street", "number": 123}, 63 | "seller_name": "John Smith", 64 | "contract_status": "DRAFT", 65 | } 66 | table.put_item(Item=contract) 67 | return table 68 | 69 | 70 | def create_test_eventbridge_bus(eventbridge): 71 | bus = eventbridge.create_event_bus(Name=EVENTBUS_NAME) 72 | return bus 73 | -------------------------------------------------------------------------------- /unicorn_properties/tests/unit/test_contract_status_changed_event_handler.py: -------------------------------------------------------------------------------- 1 | # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | # SPDX-License-Identifier: MIT-0 3 | import os 4 | from importlib import reload 5 | 6 | import pytest 7 | from unittest import mock 8 | from botocore.exceptions import ClientError 9 | 10 | from .helper import load_event, return_env_vars_dict, create_ddb_table_properties 11 | 12 | 13 | @mock.patch.dict(os.environ, return_env_vars_dict(), clear=True) 14 | def test_contract_status_changed_event_handler(dynamodb, lambda_context): 15 | eventbridge_event = load_event("eventbridge/contract_status_changed") 16 | 17 | from properties_service import contract_status_changed_event_handler 18 | 19 | # Reload is required to prevent function setup reuse from another test 20 | reload(contract_status_changed_event_handler) 21 | 22 | create_ddb_table_properties(dynamodb) 23 | 24 | ret = contract_status_changed_event_handler.lambda_handler(eventbridge_event, lambda_context) 25 | 26 | assert ret["statusCode"] == 200 27 | 28 | 29 | @mock.patch.dict(os.environ, return_env_vars_dict(), clear=True) 30 | def test_missing_property_id(dynamodb, lambda_context): 31 | eventbridge_event = {"detail": {}} 32 | 33 | from properties_service import contract_status_changed_event_handler 34 | 35 | # Reload is required to prevent function setup reuse from another test 36 | reload(contract_status_changed_event_handler) 37 | 38 | create_ddb_table_properties(dynamodb) 39 | 40 | with pytest.raises(ClientError) as e: 41 | contract_status_changed_event_handler.lambda_handler(eventbridge_event, lambda_context) 42 | 43 | assert "ValidationException" in str(e.value) 44 | -------------------------------------------------------------------------------- /unicorn_properties/tests/unit/test_properties_approval_sync_function.py: -------------------------------------------------------------------------------- 1 | # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | # SPDX-License-Identifier: MIT-0 3 | import os 4 | from importlib import reload 5 | 6 | from unittest import mock 7 | 8 | from .helper import load_event, return_env_vars_dict 9 | 10 | 11 | @mock.patch.dict(os.environ, return_env_vars_dict(), clear=True) 12 | def test_handle_status_changed_draft(stepfunction, lambda_context): 13 | ddbstream_event = load_event("ddb_stream_events/contract_status_changed_draft") 14 | 15 | from properties_service import properties_approval_sync_function 16 | 17 | reload(properties_approval_sync_function) 18 | 19 | ret = properties_approval_sync_function.lambda_handler(ddbstream_event, lambda_context) 20 | 21 | assert ret is None 22 | 23 | 24 | # NOTE: This test cannot be implemented at this time because `moto`` does not yet support mocking `stepfunctions.send_task_success` 25 | @mock.patch.dict(os.environ, return_env_vars_dict(), clear=True) 26 | def test_handle_status_changed_approved(caplog, stepfunction, lambda_context): 27 | pass 28 | # ddbstream_event = load_event('ddb_stream_events/status_approved_waiting_for_approval') 29 | 30 | # from properties_service import properties_approval_sync_function 31 | # reload(properties_approval_sync_function) 32 | 33 | # ret = properties_approval_sync_function.lambda_handler(ddbstream_event, lambda_context) 34 | 35 | # assert ret is None 36 | # assert 'Contract status for property is APPROVED' in caplog.text 37 | -------------------------------------------------------------------------------- /unicorn_properties/tests/unit/test_wait_for_contract_approval_function.py: -------------------------------------------------------------------------------- 1 | # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | # SPDX-License-Identifier: MIT-0 3 | import os 4 | from importlib import reload 5 | 6 | from unittest import mock 7 | 8 | from .helper import load_event, return_env_vars_dict, create_ddb_table_contracts_with_entry 9 | 10 | 11 | @mock.patch.dict(os.environ, return_env_vars_dict(), clear=True) 12 | def test_handle_wait_for_contract_approval_function(dynamodb, lambda_context): 13 | stepfunctions_event = load_event("lambda/wait_for_contract_approval_function") 14 | 15 | from properties_service import wait_for_contract_approval_function 16 | 17 | reload(wait_for_contract_approval_function) 18 | 19 | create_ddb_table_contracts_with_entry(dynamodb) 20 | 21 | ddbitem_before = dynamodb.Table("table1").get_item(Key={"property_id": stepfunctions_event["Input"]["property_id"]}) 22 | assert "sfn_wait_approved_task_token" not in ddbitem_before["Item"] 23 | 24 | ret = wait_for_contract_approval_function.lambda_handler(stepfunctions_event, lambda_context) 25 | ddbitem_after = dynamodb.Table("table1").get_item(Key={"property_id": stepfunctions_event["Input"]["property_id"]}) 26 | 27 | assert ret["property_id"] == stepfunctions_event["Input"]["property_id"] 28 | assert ddbitem_after["Item"]["sfn_wait_approved_task_token"] == stepfunctions_event["TaskToken"] 29 | -------------------------------------------------------------------------------- /unicorn_shared/Makefile: -------------------------------------------------------------------------------- 1 | .ONESHELL: 2 | 3 | ENVIRONMENTS = local dev prod 4 | 5 | .PHONY: deploy-namespaces deploy-images delete-namespaces delete-images 6 | 7 | deploy-namespaces: ## Deploys global Unicorn Properties namespaces for all Stages 8 | aws cloudformation create-stack --stack-name uni-prop-namespaces --template-body file://uni-prop-namespaces.yaml --capabilities CAPABILITY_AUTO_EXPAND 9 | 10 | deploy-images: ## Deploys shared images stack for local dev prod stages 11 | @for env in $(ENVIRONMENTS); do \ 12 | stage=$$env; \ 13 | if ! aws cloudformation describe-stacks --stack-name "uni-prop-$$env-images" >/dev/null 2>&1; then \ 14 | echo "Creating shared images stack for $$env environment"; \ 15 | aws cloudformation create-stack \ 16 | --stack-name "uni-prop-$$env-images" \ 17 | --template-body file://uni-prop-images.yaml \ 18 | --parameters ParameterKey=Stage,ParameterValue=$$stage \ 19 | --capabilities CAPABILITY_IAM CAPABILITY_AUTO_EXPAND || echo "Stack creation failed!"; \ 20 | fi; \ 21 | done 22 | 23 | delete-namespaces: ## Depletes Unicorn Properties namespaces 24 | aws cloudformation delete-stack --stack-name uni-prop-namespaces 25 | 26 | delete-images: ## Deletes all shared images stacks 27 | @for env in $(ENVIRONMENTS); do \ 28 | stage=$$env; \ 29 | if aws cloudformation describe-stacks --stack-name "uni-prop-$$env-images" >/dev/null 2>&1; then \ 30 | echo "Deleting shared images stack for $$env environment"; \ 31 | aws cloudformation delete-stack \ 32 | --stack-name "uni-prop-$$env-images"; \ 33 | fi; \ 34 | done 35 | -------------------------------------------------------------------------------- /unicorn_shared/uni-prop-namespaces.yaml: -------------------------------------------------------------------------------- 1 | AWSTemplateFormatVersion: "2010-09-09" 2 | Transform: AWS::Serverless-2016-10-31 3 | Description: > 4 | Global namespaces for Unicorn Properties applications and services. 5 | This only needs to be deployed once. 6 | 7 | 8 | Resources: 9 | 10 | UnicornContractsNamespaceParam: 11 | Type: AWS::SSM::Parameter 12 | Properties: 13 | Type: String 14 | Name: !Sub /uni-prop/UnicornContractsNamespace 15 | Value: "unicorn.contracts" 16 | 17 | UnicornPropertiesNamespaceParam: 18 | Type: AWS::SSM::Parameter 19 | Properties: 20 | Type: String 21 | Name: !Sub /uni-prop/UnicornPropertiesNamespace 22 | Value: "unicorn.properties" 23 | 24 | UnicornWebNamespaceParam: 25 | Type: AWS::SSM::Parameter 26 | Properties: 27 | Type: String 28 | Name: !Sub /uni-prop/UnicornWebNamespace 29 | Value: "unicorn.web" 30 | 31 | 32 | Outputs: 33 | 34 | UnicornContractsNamespace: 35 | Description: Unicorn Contracts namespace parameter 36 | Value: !Ref UnicornContractsNamespaceParam 37 | 38 | UnicornPropertiesNamespace: 39 | Description: Unicorn Properties namespace parameter 40 | Value: !Ref UnicornPropertiesNamespaceParam 41 | 42 | UnicornWebNamespace: 43 | Description: Unicorn Web namespace parameter 44 | Value: !Ref UnicornWebNamespaceParam 45 | 46 | UnicornContractsNamespaceVale: 47 | Description: Unicorn Contracts namespace parameter value 48 | Value: !GetAtt UnicornContractsNamespaceParam.Value 49 | 50 | UnicornPropertiesNamespaceValue: 51 | Description: Unicorn Properties namespace parameter value 52 | Value: !GetAtt UnicornPropertiesNamespaceParam.Value 53 | 54 | UnicornWebNamespaceValue: 55 | Description: Unicorn Web namespace parameter value 56 | Value: !GetAtt UnicornWebNamespaceParam.Value -------------------------------------------------------------------------------- /unicorn_web/.gitignore: -------------------------------------------------------------------------------- 1 | ### Linux ### 2 | **/*~ 3 | **/.fuse_hidden* 4 | **/.directory 5 | **/.Trash-* 6 | **/.nfs* 7 | 8 | ### OSX ### 9 | **/*.DS_Store 10 | **/.AppleDouble 11 | **/.LSOverride 12 | **/.DocumentRevisions-V100 13 | **/.fseventsd 14 | **/.Spotlight-V100 15 | **/.TemporaryItems 16 | **/.Trashes 17 | **/.VolumeIcon.icns 18 | **/.com.apple.timemachine.donotpresent 19 | 20 | ### JetBrains IDEs ### 21 | **/*.iws 22 | **/.idea/ 23 | **/.idea_modules/ 24 | 25 | ### Python ### 26 | **/__pycache__/ 27 | **/*.py[cod] 28 | **/*$py.class 29 | **/.Python 30 | **/build/ 31 | **/develop-eggs/ 32 | **/dist/ 33 | **/downloads/ 34 | **/eggs/ 35 | **/.eggs/ 36 | **/parts/ 37 | **/sdist/ 38 | **/wheels/ 39 | **/*.egg 40 | **/*.egg-info/ 41 | **/.installed.cfg 42 | **/pip-log.txt 43 | **/pip-delete-this-directory.txt 44 | 45 | ### Unit test / coverage reports ### 46 | **/.cache 47 | **/.coverage 48 | **/.hypothesis/ 49 | **/.pytest_cache/ 50 | **/.tox/ 51 | **/*.cover 52 | **/coverage.xml 53 | **/htmlcov/ 54 | **/nosetests.xml 55 | 56 | ### pyenv ### 57 | **/.python-version 58 | 59 | ### Environments ### 60 | **/.env 61 | **/.venv 62 | **/env/ 63 | **/venv/ 64 | **/ENV/ 65 | **/env.bak/ 66 | **/venv.bak/ 67 | 68 | ### VisualStudioCode ### 69 | **/.vscode/ 70 | **/.history 71 | 72 | ### Windows ### 73 | # Windows thumbnail cache files 74 | **/Thumbs.db 75 | **/ehthumbs.db 76 | **/ehthumbs_vista.db 77 | **/Desktop.ini 78 | **/$RECYCLE.BIN/ 79 | **/*.lnk 80 | 81 | ### requirements.txt ### 82 | # We ignore Python's requirements.txt as we use Poetry instead 83 | **/requirements.txt 84 | **/.aws-sam 85 | -------------------------------------------------------------------------------- /unicorn_web/Makefile: -------------------------------------------------------------------------------- 1 | #### Global Variables 2 | stackName := $(shell yq -r '.default.global.parameters.stack_name' samconfig.toml) 3 | 4 | 5 | #### Test Variables 6 | apiUrl = $(call cf_output,$(stackName),ApiUrl) 7 | 8 | 9 | 10 | #### Build/Deploy Tasks 11 | build: 12 | ruff format 13 | sam validate --lint 14 | cfn-lint template.yaml -a cfn_lint_serverless.rules 15 | uv export --no-hashes --format=requirements-txt --output-file=src/requirements.txt 16 | sam build -c $(DOCKER_OPTS) 17 | 18 | deps: 19 | uv sync 20 | 21 | deploy: deps build 22 | sam deploy 23 | 24 | 25 | #### Tests 26 | test: unit-test 27 | 28 | unit-test: 29 | uv run pytest tests/unit/ 30 | 31 | curl-test: 32 | $(call mcurl,GET,search/usa/anytown) 33 | $(call mcurl,GET,search/usa/anytown/main-street) 34 | $(call mcurl,GET,properties/usa/anytown/main-street/111) 35 | @echo "[DONE]" 36 | 37 | 38 | #### Utilities 39 | sync: 40 | sam sync --stack-name $(stackName) --watch 41 | 42 | logs: 43 | sam logs -t 44 | 45 | clean: 46 | find . -type d -name __pycache__ -exec rm -rf {} \; 2>/dev/null || true 47 | find . -type f -name requirements.txt -exec rm -f {} \; 2>/dev/null || true 48 | rm -rf .pytest_cache/ .aws-sam/ || true 49 | 50 | delete: 51 | sam delete --stack-name $(stackName) --no-prompts 52 | 53 | ci_init: 54 | uv venv 55 | uv export --no-hashes --format=requirements.txt --output-file=src/requirements.txt --extra=dev 56 | uv pip install -r src/requirements.txt 57 | 58 | 59 | #### Helper Functions 60 | define mcurl 61 | curl -s -X $(1) -H "Content-type: application/json" $(apiUrl)$(2) | jq 62 | endef 63 | 64 | define cf_output 65 | $(shell aws cloudformation describe-stacks \ 66 | --output text \ 67 | --stack-name $(1) \ 68 | --query 'Stacks[0].Outputs[?OutputKey==`$(2)`].OutputValue') 69 | endef 70 | -------------------------------------------------------------------------------- /unicorn_web/README.md: -------------------------------------------------------------------------------- 1 | # Developing Unicorn Web 2 | 3 | ![Properties Web Architecture](https://static.us-east-1.prod.workshops.aws/public/f273b5fc-17cd-406b-9e63-1d331b00589d/static/images/architecture-properties-web.png) 4 | 5 | ## Architecture Overview 6 | 7 | Unicorn Web is primarily responsible for allowing customers to search and view property listings. It also supports ability for agents to request approval for specific property. Those approval requests are sent to Property service for validation, before Properties table is updated with approval evaluation results. 8 | 9 | A core component of Unicorn Web are the Lambda functions which are responsible with completing API Gateway requests to: 10 | 11 | - search approved property listings 12 | This function interacts with DynamoDB table to retrieve property listings marked as `APPROVED`. The API Gateway implementation and lambda code support multiple types of search patterns, and allow searching by city, street, or house number. 13 | 14 | - request approval of property listing 15 | This function sends an event to EventBridge requesting an approval for a property listing specified in the payload sent from client 16 | 17 | - publication approved function 18 | There is also a lambda function responsible for receiving any "Approval Evaluation Completed" events from EventBridge. This function writes the evaluation result to DynamoDB table. 19 | -------------------------------------------------------------------------------- /unicorn_web/data/load_data.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | ROOT_DIR="$(cd -- "$(dirname "$0")/../" >/dev/null 2>&1 ; pwd -P )" 4 | STACK_NAME="$(yq -ot '.default.global.parameters.stack_name' $ROOT_DIR/samconfig.toml)" 5 | 6 | JSON_FILE="$ROOT_DIR/data/property_data.json" 7 | echo "JSON_FILE: '${JSON_FILE}'" 8 | 9 | DDB_TBL_NAME="$(aws cloudformation describe-stacks --stack-name ${STACK_NAME} --query 'Stacks[0].Outputs[?ends_with(OutputKey, `WebTableName`)].OutputValue' --output text)" 10 | echo "DDB_TABLE_NAME: '${DDB_TBL_NAME}'" 11 | 12 | echo "LOADING ITEMS TO DYNAMODB:" 13 | aws ddb put ${DDB_TBL_NAME} file://${JSON_FILE} 14 | echo "DONE!" 15 | -------------------------------------------------------------------------------- /unicorn_web/data/property_data.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "PK": "PROPERTY#usa#anytown", 4 | "SK": "main-street#111", 5 | "country": "USA", 6 | "city": "Anytown", 7 | "street": "Main Street", 8 | "number": 111, 9 | "description": "This classic Anytown estate comes with a covetable lake view. The romantic and comfortable backyard is the perfect setting for unicorn get-togethers. The open concept Main Stable is fully equipped with all the desired amenities. Second floor features 6 straw bales including large Rainbow Suite with private training pool terrace and Jr Sparkles Suite.", 10 | "contract": "sale", 11 | "listprice": 200, 12 | "currency": "SPL", 13 | "images": [ 14 | "property_images/prop1_exterior1.jpg", 15 | "property_images/prop1_interior1.jpg", 16 | "property_images/prop1_interior2.jpg", 17 | "property_images/prop1_interior3.jpg" 18 | ], 19 | "status": "PENDING" 20 | }, 21 | { 22 | "PK": "PROPERTY#usa#main-town", 23 | "SK": "main-street#222", 24 | "country": "USA", 25 | "city": "Main Town", 26 | "street": "My Street", 27 | "number": 222, 28 | "description": "This picturesque Main Town property is ideally suited for the DIY enthusiast unicorns. The antique art-nouveau pillars at the entrance offer a reminder of historic times. This property comes uniquely suited with no electricity, offering a tranquil environment with no digital disruptions. The rooftop has only minor fire damages from fireworks incidents during the 1970s.", 29 | "contract": "sale", 30 | "listprice": 150, 31 | "currency": "SPL", 32 | "images": [ 33 | "property_images/prop2_exterior1.jpg", 34 | "property_images/prop2_exterior2.jpg", 35 | "property_images/prop2_interior1.jpg", 36 | "property_images/prop2_interior2.jpg" 37 | ], 38 | "status": "PENDING" 39 | }, 40 | { 41 | "PK": "PROPERTY#usa#anytown", 42 | "SK": "main-street#333", 43 | "country": "USA", 44 | "city": "Anytown", 45 | "street": "Main Street", 46 | "number": 333, 47 | "description": "State of the art mansion featuring the most luxurious facilities, not only of Anytown but the whole Anytown region. The garden is equipped with three separate landing pads; one for the owner, one for guests and one for employees of the owner. The estate has its own satellite dish receiver and is equipped with double fiber connections for high-speed internet. The house has both an indoor and an outdoor swimming pool with automated pool cleaning facilities.", 48 | "contract": "sale", 49 | "listprice": 250, 50 | "currency": "SPL", 51 | "images": [ 52 | "property_images/prop3_exterior1.jpg", 53 | "property_images/prop3_interior1.jpg", 54 | "property_images/prop3_interior2.jpg", 55 | "property_images/prop3_interior3.jpg" 56 | ], 57 | "status": "PENDING" 58 | } 59 | ] -------------------------------------------------------------------------------- /unicorn_web/integration/PublicationApprovalRequested.json: -------------------------------------------------------------------------------- 1 | { 2 | "openapi": "3.0.0", 3 | "info": { 4 | "version": "1.0.0", 5 | "title": "PublicationApprovalRequested" 6 | }, 7 | "paths": {}, 8 | "components": { 9 | "schemas": { 10 | "AWSEvent": { 11 | "type": "object", 12 | "required": [ 13 | "detail-type", 14 | "resources", 15 | "detail", 16 | "id", 17 | "source", 18 | "time", 19 | "region", 20 | "version", 21 | "account" 22 | ], 23 | "x-amazon-events-detail-type": "PublicationApprovalRequested", 24 | "x-amazon-events-source": "unicorn.web", 25 | "properties": { 26 | "detail": { 27 | "$ref": "#/components/schemas/PublicationApprovalRequested" 28 | }, 29 | "account": { 30 | "type": "string" 31 | }, 32 | "detail-type": { 33 | "type": "string" 34 | }, 35 | "id": { 36 | "type": "string" 37 | }, 38 | "region": { 39 | "type": "string" 40 | }, 41 | "resources": { 42 | "type": "array", 43 | "items": { 44 | "type": "string" 45 | } 46 | }, 47 | "source": { 48 | "type": "string" 49 | }, 50 | "time": { 51 | "type": "string", 52 | "format": "date-time" 53 | }, 54 | "version": { 55 | "type": "string" 56 | } 57 | } 58 | }, 59 | "PublicationApprovalRequested": { 60 | "type": "object", 61 | "required": [ 62 | "images", 63 | "address", 64 | "listprice", 65 | "contract", 66 | "description", 67 | "currency", 68 | "property_id", 69 | "status" 70 | ], 71 | "properties": { 72 | "address": { 73 | "$ref": "#/components/schemas/Address" 74 | }, 75 | "contract": { 76 | "type": "string" 77 | }, 78 | "currency": { 79 | "type": "string" 80 | }, 81 | "description": { 82 | "type": "string" 83 | }, 84 | "images": { 85 | "type": "array", 86 | "items": { 87 | "type": "string" 88 | } 89 | }, 90 | "listprice": { 91 | "type": "string" 92 | }, 93 | "property_id": { 94 | "type": "string" 95 | }, 96 | "status": { 97 | "type": "string" 98 | } 99 | } 100 | }, 101 | "Address": { 102 | "type": "object", 103 | "required": [ 104 | "country", 105 | "number", 106 | "city", 107 | "street" 108 | ], 109 | "properties": { 110 | "city": { 111 | "type": "string" 112 | }, 113 | "country": { 114 | "type": "string" 115 | }, 116 | "number": { 117 | "type": "string" 118 | }, 119 | "street": { 120 | "type": "string" 121 | } 122 | } 123 | } 124 | } 125 | } 126 | } 127 | -------------------------------------------------------------------------------- /unicorn_web/integration/subscriber-policies.yaml: -------------------------------------------------------------------------------- 1 | # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | # SPDX-License-Identifier: MIT-0 3 | AWSTemplateFormatVersion: '2010-09-09' 4 | Description: > 5 | Defines the event bus policies that determine who can create rules on the event bus to 6 | subscribe to events published by Unicorn Web Service. 7 | 8 | Parameters: 9 | Stage: 10 | Type: String 11 | Default: local 12 | AllowedValues: 13 | - local 14 | - dev 15 | - prod 16 | 17 | Resources: 18 | # Update this policy as you get new subscribers by adding their namespace to events:source 19 | CrossServiceCreateRulePolicy: 20 | Type: AWS::Events::EventBusPolicy 21 | Properties: 22 | EventBusName: 23 | Fn::Sub: "{{resolve:ssm:/uni-prop/${Stage}/UnicornWebEventBus}}" 24 | StatementId: 25 | Fn::Sub: "OnlyRulesForWebServiceEvents-${Stage}" 26 | Statement: 27 | Effect: Allow 28 | Principal: 29 | AWS: 30 | Fn::Sub: "arn:${AWS::Partition}:iam::${AWS::AccountId}:root" 31 | Action: 32 | - events:PutRule 33 | - events:DeleteRule 34 | - events:DescribeRule 35 | - events:DisableRule 36 | - events:EnableRule 37 | - events:PutTargets 38 | - events:RemoveTargets 39 | Resource: 40 | - Fn::Sub: 41 | - arn:${AWS::Partition}:events:${AWS::Region}:${AWS::AccountId}:rule/${eventBusName}/* 42 | - eventBusName: 43 | Fn::Sub: "{{resolve:ssm:/uni-prop/${Stage}/UnicornWebEventBus}}" 44 | Condition: 45 | StringEqualsIfExists: 46 | "events:creatorAccount": "${aws:PrincipalAccount}" 47 | StringEquals: 48 | "events:source": 49 | - "{{resolve:ssm:/uni-prop/UnicornWebNamespace}}" 50 | "Null": 51 | "events:source": "false" 52 | -------------------------------------------------------------------------------- /unicorn_web/integration/subscriptions.yaml: -------------------------------------------------------------------------------- 1 | # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | # SPDX-License-Identifier: MIT-0 3 | AWSTemplateFormatVersion: '2010-09-09' 4 | Description: Defines the rule for the events (subscriptions) that Unicorn Web wants to consume. 5 | 6 | Parameters: 7 | Stage: 8 | Type: String 9 | Default: local 10 | AllowedValues: 11 | - local 12 | - dev 13 | - prod 14 | 15 | Resources: 16 | #### UNICORN PROPERTIES EVENT SUBSCRIPTIONS 17 | PublicationEvaluationCompletedSubscriptionRule: 18 | Type: AWS::Events::Rule 19 | Properties: 20 | Name: unicorn.web-PublicationEvaluationCompleted 21 | Description: PublicationEvaluationCompleted subscription 22 | EventBusName: 23 | Fn::Sub: "{{resolve:ssm:/uni-prop/${Stage}/UnicornPropertiesEventBusArn}}" 24 | EventPattern: 25 | source: 26 | - "{{resolve:ssm:/uni-prop/UnicornPropertiesNamespace}}" 27 | detail-type: 28 | - PublicationEvaluationCompleted 29 | State: ENABLED 30 | Targets: 31 | - Id: SendEventTo 32 | Arn: 33 | Fn::Sub: "{{resolve:ssm:/uni-prop/${Stage}/UnicornWebEventBusArn}}" 34 | RoleArn: 35 | Fn::GetAtt: UnicornPropertiesEventBusToUnicornWebEventBusRole.Arn 36 | 37 | # This IAM role allows EventBridge to assume the permissions necessary to send events 38 | # from the publishing event bus, to the subscribing event bus (UnicornWebEventBusArn) 39 | UnicornPropertiesEventBusToUnicornWebEventBusRole: 40 | Type: AWS::IAM::Role 41 | Properties: 42 | AssumeRolePolicyDocument: 43 | Statement: 44 | - Effect: Allow 45 | Action: sts:AssumeRole 46 | Principal: 47 | Service: events.amazonaws.com 48 | Policies: 49 | - PolicyName: PutEventsOnUnicornWebEventBus 50 | PolicyDocument: 51 | Version: "2012-10-17" 52 | Statement: 53 | - Effect: Allow 54 | Action: events:PutEvents 55 | Resource: 56 | Fn::Sub: "{{resolve:ssm:/uni-prop/${Stage}/UnicornWebEventBusArn}}" 57 | 58 | Outputs: 59 | PublicationEvaluationCompletedSubscription: 60 | Description: Rule ARN for Property service event subscription 61 | Value: 62 | Fn::GetAtt: PublicationEvaluationCompletedSubscriptionRule.Arn 63 | -------------------------------------------------------------------------------- /unicorn_web/pyproject.toml: -------------------------------------------------------------------------------- 1 | [project] 2 | name = "web_service" 3 | version = "0.2.0" 4 | description = "Unicorn Properties Web Service" 5 | authors = [ 6 | {name = "Amazon Web Services"} 7 | ] 8 | readme = "README.md" 9 | requires-python = ">=3.12" 10 | 11 | dependencies = [ 12 | "aws-lambda-powertools[tracer]>=3.9.0", 13 | "aws-xray-sdk>=2.14.0", 14 | "boto3>=1.37.23", 15 | "requests>=2.32.3", 16 | "crhelper>=2.0.11", 17 | ] 18 | 19 | [project.optional-dependencies] 20 | dev = [ 21 | "aws-lambda-powertools[all]>=3.9.0", 22 | "moto[dynamodb,events,sqs]>=5.0.14", 23 | "importlib-metadata>=8.4.0", 24 | "pyyaml>=6.0.2", 25 | "arnparse>=0.0.2", 26 | "pytest>=8.3.4", 27 | "ruff>=0.9.7", 28 | ] 29 | 30 | [tool.setuptools] 31 | package-dir = {"web_service" = "src"} 32 | packages = ["web_service"] 33 | -------------------------------------------------------------------------------- /unicorn_web/pytest.ini: -------------------------------------------------------------------------------- 1 | [pytest] 2 | pythonpath = src 3 | -------------------------------------------------------------------------------- /unicorn_web/ruff.toml: -------------------------------------------------------------------------------- 1 | # Exclude files/directories from analysis 2 | exclude = [ 3 | ".bzr", 4 | ".direnv", 5 | ".eggs", 6 | ".git", 7 | ".git-rewrite", 8 | ".hg", 9 | ".ipynb_checkpoints", 10 | ".mypy_cache", 11 | ".nox", 12 | ".pants.d", 13 | ".pyenv", 14 | ".pytest_cache", 15 | ".pytype", 16 | ".ruff_cache", 17 | ".svn", 18 | ".tox", 19 | ".venv", 20 | ".vscode", 21 | "__pypackages__", 22 | "_build", 23 | "buck-out", 24 | "build", 25 | "dist", 26 | "node_modules", 27 | "site-packages", 28 | "venv", 29 | ] 30 | 31 | # Same as Black. 32 | line-length = 120 33 | indent-width = 4 34 | 35 | fix = true 36 | 37 | [format] 38 | # Like Black, use double quotes for strings. 39 | quote-style = "double" 40 | 41 | # Like Black, indent with spaces, rather than tabs. 42 | indent-style = "space" 43 | 44 | # Like Black, respect magic trailing commas. 45 | skip-magic-trailing-comma = false 46 | 47 | # Like Black, automatically detect the appropriate line ending. 48 | line-ending = "auto" 49 | 50 | 51 | [lint] 52 | # 1. Enable flake8-bugbear (`B`) rules, in addition to the defaults. 53 | select = ["E4", "E7", "E9", "F", "B"] 54 | 55 | # 2. Avoid enforcing line-length violations (`E501`) 56 | ignore = ["E501"] 57 | 58 | # 3. Avoid trying to fix flake8-bugbear (`B`) violations. 59 | unfixable = ["B"] 60 | 61 | # 4. Ignore `E402` (import violations) in all `__init__.py` files, and in selected subdirectories. 62 | [lint.per-file-ignores] 63 | "__init__.py" = ["E402"] 64 | "**/{tests,docs,tools}/*" = ["E402"] -------------------------------------------------------------------------------- /unicorn_web/samconfig.toml: -------------------------------------------------------------------------------- 1 | version = 0.1 2 | 3 | [default.global.parameters] 4 | stack_name = "uni-prop-local-web" 5 | s3_prefix = "uni-prop-local-web" 6 | resolve_s3 = true 7 | resolve_image_repositories = true 8 | 9 | [default.build.parameters] 10 | cached = true 11 | parallel = true 12 | 13 | [default.deploy.parameters] 14 | disable_rollback = true 15 | confirm_changeset = false 16 | fail_on_empty_changeset = false 17 | capabilities = ["CAPABILITY_IAM", "CAPABILITY_AUTO_EXPAND"] 18 | parameter_overrides = ["Stage=local"] 19 | 20 | [default.validate.parameters] 21 | lint = true 22 | 23 | [default.sync.parameters] 24 | watch = true 25 | 26 | [default.local_start_api.parameters] 27 | warm_containers = "EAGER" 28 | 29 | [default.local_start_lambda.parameters] 30 | warm_containers = "EAGER" -------------------------------------------------------------------------------- /unicorn_web/src/approvals_service/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/aws-serverless-developer-experience-workshop-python/c3e988cd9a2af13b672c1c79d122568333b4e48c/unicorn_web/src/approvals_service/__init__.py -------------------------------------------------------------------------------- /unicorn_web/src/schema/unicorn_properties/publicationevaluationcompleted/PublicationEvaluationCompleted.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | import pprint 3 | import re # noqa: F401 4 | 5 | import six 6 | from enum import Enum 7 | 8 | 9 | class PublicationEvaluationCompleted(object): 10 | _types = {"evaluation_result": "str", "property_id": "str"} 11 | 12 | _attribute_map = {"evaluation_result": "evaluation_result", "property_id": "property_id"} 13 | 14 | def __init__(self, evaluation_result=None, property_id=None): # noqa: E501 15 | self._evaluation_result = None 16 | self._property_id = None 17 | self.discriminator = None 18 | self.evaluation_result = evaluation_result 19 | self.property_id = property_id 20 | 21 | @property 22 | def evaluation_result(self): 23 | return self._evaluation_result 24 | 25 | @evaluation_result.setter 26 | def evaluation_result(self, evaluation_result): 27 | self._evaluation_result = evaluation_result 28 | 29 | @property 30 | def property_id(self): 31 | return self._property_id 32 | 33 | @property_id.setter 34 | def property_id(self, property_id): 35 | self._property_id = property_id 36 | 37 | def to_dict(self): 38 | result = {} 39 | 40 | for attr, _ in six.iteritems(self._types): 41 | value = getattr(self, attr) 42 | if isinstance(value, list): 43 | result[attr] = list(map(lambda x: x.to_dict() if hasattr(x, "to_dict") else x, value)) 44 | elif hasattr(value, "to_dict"): 45 | result[attr] = value.to_dict() 46 | elif isinstance(value, dict): 47 | result[attr] = dict( 48 | map( 49 | lambda item: (item[0], item[1].to_dict()) if hasattr(item[1], "to_dict") else item, 50 | value.items(), 51 | ) 52 | ) 53 | else: 54 | result[attr] = value 55 | if issubclass(PublicationEvaluationCompleted, dict): 56 | for key, value in self.items(): 57 | result[key] = value 58 | 59 | return result 60 | 61 | def to_str(self): 62 | return pprint.pformat(self.to_dict()) 63 | 64 | def __repr__(self): 65 | return self.to_str() 66 | 67 | def __eq__(self, other): 68 | if not isinstance(other, PublicationEvaluationCompleted): 69 | return False 70 | 71 | return self.__dict__ == other.__dict__ 72 | 73 | def __ne__(self, other): 74 | return not self == other 75 | -------------------------------------------------------------------------------- /unicorn_web/src/schema/unicorn_properties/publicationevaluationcompleted/__init__.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | 3 | from __future__ import absolute_import 4 | 5 | from schema.unicorn_properties.publicationevaluationcompleted.marshaller import Marshaller 6 | from schema.unicorn_properties.publicationevaluationcompleted.AWSEvent import AWSEvent 7 | from schema.unicorn_properties.publicationevaluationcompleted.PublicationEvaluationCompleted import ( 8 | PublicationEvaluationCompleted, 9 | ) 10 | -------------------------------------------------------------------------------- /unicorn_web/src/search_service/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/aws-serverless-developer-experience-workshop-python/c3e988cd9a2af13b672c1c79d122568333b4e48c/unicorn_web/src/search_service/__init__.py -------------------------------------------------------------------------------- /unicorn_web/tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/aws-serverless-developer-experience-workshop-python/c3e988cd9a2af13b672c1c79d122568333b4e48c/unicorn_web/tests/__init__.py -------------------------------------------------------------------------------- /unicorn_web/tests/events/eventbridge/put_event_publication_evaluation_completed.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "Source": "unicorn.properties", 4 | "Detail": "{\"property_id\":\"usa/anytown/main-street/111\",\"evaluation_result\": \"APPROVED\"}", 5 | "DetailType": "PublicationEvaluationCompleted", 6 | "EventBusName": "UnicornWebBus-local" 7 | } 8 | ] -------------------------------------------------------------------------------- /unicorn_web/tests/unit/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/aws-serverless-developer-experience-workshop-python/c3e988cd9a2af13b672c1c79d122568333b4e48c/unicorn_web/tests/unit/__init__.py -------------------------------------------------------------------------------- /unicorn_web/tests/unit/conftest.py: -------------------------------------------------------------------------------- 1 | # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | # SPDX-License-Identifier: MIT-0 3 | import os 4 | 5 | import boto3 6 | from aws_lambda_powertools.utilities.typing import LambdaContext 7 | 8 | import pytest 9 | from moto import mock_aws 10 | 11 | 12 | @pytest.fixture(scope="function") 13 | def aws_credentials(): 14 | """Mocked AWS Credentials for moto.""" 15 | os.environ["AWS_ACCESS_KEY_ID"] = "testing" 16 | os.environ["AWS_SECRET_ACCESS_KEY"] = "testing" 17 | os.environ["AWS_SECURITY_TOKEN"] = "testing" 18 | os.environ["AWS_SESSION_TOKEN"] = "testing" 19 | 20 | 21 | @pytest.fixture(scope="function") 22 | def env_vars(): 23 | os.environ["POWERTOOLS_SERVICE_NAME"] = "unicorn.contracts" 24 | os.environ["SERVICE_NAMESPACE"] = "unicorn.contracts" 25 | os.environ["POWERTOOLS_SERVICE_NAME"] = "unicorn.contracts" 26 | os.environ["POWERTOOLS_TRACE_DISABLED"] = "true" 27 | os.environ["POWERTOOLS_LOGGER_LOG_EVENT"] = "Info" 28 | os.environ["POWERTOOLS_LOGGER_SAMPLE_RATE"] = "0.1" 29 | os.environ["POWERTOOLS_METRICS_NAMESPACE"] = "unicorn.contracts" 30 | os.environ["LOG_LEVEL"] = "INFO" 31 | 32 | 33 | @pytest.fixture(scope="function") 34 | def dynamodb(aws_credentials): 35 | with mock_aws(): 36 | yield boto3.resource("dynamodb", region_name="ap-southeast-2") 37 | 38 | 39 | @pytest.fixture(scope="function") 40 | def eventbridge(aws_credentials): 41 | with mock_aws(): 42 | yield boto3.client("events", region_name="ap-southeast-2") 43 | 44 | 45 | @pytest.fixture(scope="function") 46 | def sqs(aws_credentials): 47 | with mock_aws(): 48 | yield boto3.client("sqs", region_name="ap-southeast-2") 49 | 50 | 51 | @pytest.fixture(scope="function") 52 | def lambda_context(): 53 | context: LambdaContext = LambdaContext() 54 | context._function_name = "contractsService-CreateContractFunction-IWaQgsTEtLtX" 55 | context._function_version = "$LATEST" 56 | context._invoked_function_arn = ( 57 | "arn:aws:lambda:ap-southeast-2:424490683636:function:contractsService-CreateContractFunction-IWaQgsTEtLtX" 58 | ) 59 | context._memory_limit_in_mb = 128 60 | context._aws_request_id = "6f970d26-71d6-4c87-a196-9375f85c7b07" 61 | context._log_group_name = "/aws/lambda/contractsService-CreateContractFunction-IWaQgsTEtLtX" 62 | context._log_stream_name = "2022/07/14/[$LATEST]7c71ca59882b4c569dd007c7e41c81e8" 63 | # context._identity=CognitoIdentity([cognito_identity_id=None,cognito_identity_pool_id=None])]) 64 | # context._client_context=None 65 | return context 66 | -------------------------------------------------------------------------------- /unicorn_web/tests/unit/events/property_approved.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0", 3 | "id": "a1b2c3d4-5678-90ab-cdef-EXAMPLEaaaaa", 4 | "detail-type": "PublicationEvaluationCompleted", 5 | "source": "unicorn.properties", 6 | "account": "111122223333", 7 | "time": "2022-12-01T01:01:01Z", 8 | "region": "ap-southeast-1", 9 | "resources": [ 10 | "arn:aws:states:ap-southeast-1:111122223333:stateMachine:testStateMachine", 11 | "arn:aws:states:ap-southeast-1:111122223333:execution:testmyStateMachine:a1b2c3d4-5678-90ab-cdef-EXAMPLEbbbbb" 12 | ], 13 | "detail": { 14 | "evaluation_result": "APPROVED", 15 | "property_id": "usa/anytown/main-street/126" 16 | } 17 | } -------------------------------------------------------------------------------- /unicorn_web/tests/unit/events/request_already_approved.json: -------------------------------------------------------------------------------- 1 | { 2 | "resource": "/request_approval", 3 | "path": "/request_approval", 4 | "body": "{\n \"property_id\": \"usa/anytown/main-street/124\"\n}", 5 | "httpMethod": "POST", 6 | "headers": { 7 | "Accept": "*/*", 8 | "Accept-Encoding": "gzip, deflate, br", 9 | "Cache-Control": "no-cache", 10 | "Content-Type": "application/json", 11 | "Host": "test_api_id.execute-api.ap-southeast-1.amazonaws.com", 12 | "User-Agent": "PyTest", 13 | "X-Forwarded-For": "203.0.113.123", 14 | "X-Forwarded-Port": "443", 15 | "X-Forwarded-Proto": "https" 16 | }, 17 | "multiValueHeaders": { 18 | "Accept": [ 19 | "*/*" 20 | ], 21 | "Accept-Encoding": [ 22 | "gzip, deflate, br" 23 | ], 24 | "Cache-Control": [ 25 | "no-cache" 26 | ], 27 | "Content-Type": [ 28 | "application/json" 29 | ], 30 | "Host": [ 31 | "test_api_id.execute-api.ap-southeast-1.amazonaws.com" 32 | ], 33 | "User-Agent": [ 34 | "PyTest" 35 | ], 36 | "X-Forwarded-For": [ 37 | "203.0.113.123" 38 | ], 39 | "X-Forwarded-Port": [ 40 | "443" 41 | ], 42 | "X-Forwarded-Proto": [ 43 | "https" 44 | ] 45 | }, 46 | "queryStringParameters": null, 47 | "multiValueQueryStringParameters": null, 48 | "pathParameters": null, 49 | "stageVariables": null, 50 | "requestContext": { 51 | "resourceId": "resource", 52 | "resourcePath": "/request_approval", 53 | "httpMethod": "POST", 54 | "extendedRequestId": "testReqId=", 55 | "requestTime": "01/Dec/2022:01:01:01 +0000", 56 | "path": "/Local/request_approval", 57 | "accountId": "111122223333", 58 | "protocol": "HTTP/1.1", 59 | "stage": "Local", 60 | "domainPrefix": "test_api_id", 61 | "requestTimeEpoch": 1669856461000, 62 | "requestId": "a1b2c3d4-5678-90ab-cdef-EXAMPLE11111", 63 | "identity": { 64 | "cognitoIdentityPoolId": null, 65 | "accountId": null, 66 | "cognitoIdentityId": null, 67 | "caller": null, 68 | "sourceIp": "203.0.113.123", 69 | "principalOrgId": null, 70 | "accessKey": null, 71 | "cognitoAuthenticationType": null, 72 | "cognitoAuthenticationProvider": null, 73 | "userArn": null, 74 | "userAgent": "PyTest", 75 | "user": null 76 | }, 77 | "domainName": "test_api_id.execute-api.ap-southeast-1.amazonaws.com", 78 | "apiId": "test_api_id" 79 | }, 80 | "isBase64Encoded": false 81 | } -------------------------------------------------------------------------------- /unicorn_web/tests/unit/events/request_approval_bad_input.json: -------------------------------------------------------------------------------- 1 | { 2 | "resource": "/request_approval", 3 | "path": "/request_approval", 4 | "body": "NOT VALID JSON", 5 | "httpMethod": "POST", 6 | "headers": { 7 | "Accept": "*/*", 8 | "Accept-Encoding": "gzip, deflate, br", 9 | "Cache-Control": "no-cache", 10 | "Content-Type": "application/json", 11 | "Host": "test_api_id.execute-api.ap-southeast-1.amazonaws.com", 12 | "User-Agent": "PyTest", 13 | "X-Forwarded-For": "203.0.113.123", 14 | "X-Forwarded-Port": "443", 15 | "X-Forwarded-Proto": "https" 16 | }, 17 | "multiValueHeaders": { 18 | "Accept": [ 19 | "*/*" 20 | ], 21 | "Accept-Encoding": [ 22 | "gzip, deflate, br" 23 | ], 24 | "Cache-Control": [ 25 | "no-cache" 26 | ], 27 | "Content-Type": [ 28 | "application/json" 29 | ], 30 | "Host": [ 31 | "test_api_id.execute-api.ap-southeast-1.amazonaws.com" 32 | ], 33 | "User-Agent": [ 34 | "PyTest" 35 | ], 36 | "X-Forwarded-For": [ 37 | "203.0.113.123" 38 | ], 39 | "X-Forwarded-Port": [ 40 | "443" 41 | ], 42 | "X-Forwarded-Proto": [ 43 | "https" 44 | ] 45 | }, 46 | "queryStringParameters": null, 47 | "multiValueQueryStringParameters": null, 48 | "pathParameters": null, 49 | "stageVariables": null, 50 | "requestContext": { 51 | "resourceId": "resource", 52 | "resourcePath": "/request_approval", 53 | "httpMethod": "POST", 54 | "extendedRequestId": "testReqId=", 55 | "requestTime": "01/Dec/2022:01:01:01 +0000", 56 | "path": "/Local/request_approval", 57 | "accountId": "111122223333", 58 | "protocol": "HTTP/1.1", 59 | "stage": "Local", 60 | "domainPrefix": "test_api_id", 61 | "requestTimeEpoch": 1669856461000, 62 | "requestId": "a1b2c3d4-5678-90ab-cdef-EXAMPLE11111", 63 | "identity": { 64 | "cognitoIdentityPoolId": null, 65 | "accountId": null, 66 | "cognitoIdentityId": null, 67 | "caller": null, 68 | "sourceIp": "203.0.113.123", 69 | "principalOrgId": null, 70 | "accessKey": null, 71 | "cognitoAuthenticationType": null, 72 | "cognitoAuthenticationProvider": null, 73 | "userArn": null, 74 | "userAgent": "PyTest", 75 | "user": null 76 | }, 77 | "domainName": "test_api_id.execute-api.ap-southeast-1.amazonaws.com", 78 | "apiId": "test_api_id" 79 | }, 80 | "isBase64Encoded": false 81 | } -------------------------------------------------------------------------------- /unicorn_web/tests/unit/events/request_approval_event.json: -------------------------------------------------------------------------------- 1 | { 2 | "property_id": "usa/anytown/main-street/123" 3 | } -------------------------------------------------------------------------------- /unicorn_web/tests/unit/events/request_invalid_property_id.json: -------------------------------------------------------------------------------- 1 | { 2 | "resource": "/request_approval", 3 | "path": "/request_approval", 4 | "body": "{\n \"property_id\": \"usa/anytown/Main-street/122\"\n}", 5 | "httpMethod": "POST", 6 | "headers": { 7 | "Accept": "*/*", 8 | "Accept-Encoding": "gzip, deflate, br", 9 | "Cache-Control": "no-cache", 10 | "Content-Type": "application/json", 11 | "Host": "test_api_id.execute-api.ap-southeast-1.amazonaws.com", 12 | "User-Agent": "PyTest", 13 | "X-Forwarded-For": "203.0.113.123", 14 | "X-Forwarded-Port": "443", 15 | "X-Forwarded-Proto": "https" 16 | }, 17 | "multiValueHeaders": { 18 | "Accept": [ 19 | "*/*" 20 | ], 21 | "Accept-Encoding": [ 22 | "gzip, deflate, br" 23 | ], 24 | "Cache-Control": [ 25 | "no-cache" 26 | ], 27 | "Content-Type": [ 28 | "application/json" 29 | ], 30 | "Host": [ 31 | "test_api_id.execute-api.ap-southeast-1.amazonaws.com" 32 | ], 33 | "User-Agent": [ 34 | "PyTest" 35 | ], 36 | "X-Forwarded-For": [ 37 | "203.0.113.123" 38 | ], 39 | "X-Forwarded-Port": [ 40 | "443" 41 | ], 42 | "X-Forwarded-Proto": [ 43 | "https" 44 | ] 45 | }, 46 | "queryStringParameters": null, 47 | "multiValueQueryStringParameters": null, 48 | "pathParameters": null, 49 | "stageVariables": null, 50 | "requestContext": { 51 | "resourceId": "resource", 52 | "resourcePath": "/request_approval", 53 | "httpMethod": "POST", 54 | "extendedRequestId": "testReqId=", 55 | "requestTime": "01/Dec/2022:01:01:01 +0000", 56 | "path": "/Local/request_approval", 57 | "accountId": "111122223333", 58 | "protocol": "HTTP/1.1", 59 | "stage": "Local", 60 | "domainPrefix": "test_api_id", 61 | "requestTimeEpoch": 1669856461000, 62 | "requestId": "a1b2c3d4-5678-90ab-cdef-EXAMPLE11111", 63 | "identity": { 64 | "cognitoIdentityPoolId": null, 65 | "accountId": null, 66 | "cognitoIdentityId": null, 67 | "caller": null, 68 | "sourceIp": "203.0.113.123", 69 | "principalOrgId": null, 70 | "accessKey": null, 71 | "cognitoAuthenticationType": null, 72 | "cognitoAuthenticationProvider": null, 73 | "userArn": null, 74 | "userAgent": "PyTest", 75 | "user": null 76 | }, 77 | "domainName": "test_api_id.execute-api.ap-southeast-1.amazonaws.com", 78 | "apiId": "test_api_id" 79 | }, 80 | "isBase64Encoded": false 81 | } -------------------------------------------------------------------------------- /unicorn_web/tests/unit/events/request_non_existent_property.json: -------------------------------------------------------------------------------- 1 | { 2 | "resource": "/request_approval", 3 | "path": "/request_approval", 4 | "body": "{\n \"property_id\": \"usa/anytown/main-street/122\"\n}", 5 | "httpMethod": "POST", 6 | "headers": { 7 | "Accept": "*/*", 8 | "Accept-Encoding": "gzip, deflate, br", 9 | "Cache-Control": "no-cache", 10 | "Content-Type": "application/json", 11 | "Host": "test_api_id.execute-api.ap-southeast-1.amazonaws.com", 12 | "User-Agent": "PyTest", 13 | "X-Forwarded-For": "203.0.113.123", 14 | "X-Forwarded-Port": "443", 15 | "X-Forwarded-Proto": "https" 16 | }, 17 | "multiValueHeaders": { 18 | "Accept": [ 19 | "*/*" 20 | ], 21 | "Accept-Encoding": [ 22 | "gzip, deflate, br" 23 | ], 24 | "Cache-Control": [ 25 | "no-cache" 26 | ], 27 | "Content-Type": [ 28 | "application/json" 29 | ], 30 | "Host": [ 31 | "test_api_id.execute-api.ap-southeast-1.amazonaws.com" 32 | ], 33 | "User-Agent": [ 34 | "PyTest" 35 | ], 36 | "X-Forwarded-For": [ 37 | "203.0.113.123" 38 | ], 39 | "X-Forwarded-Port": [ 40 | "443" 41 | ], 42 | "X-Forwarded-Proto": [ 43 | "https" 44 | ] 45 | }, 46 | "queryStringParameters": null, 47 | "multiValueQueryStringParameters": null, 48 | "pathParameters": null, 49 | "stageVariables": null, 50 | "requestContext": { 51 | "resourceId": "resource", 52 | "resourcePath": "/request_approval", 53 | "httpMethod": "POST", 54 | "extendedRequestId": "testReqId=", 55 | "requestTime": "01/Dec/2022:01:01:01 +0000", 56 | "path": "/Local/request_approval", 57 | "accountId": "111122223333", 58 | "protocol": "HTTP/1.1", 59 | "stage": "Local", 60 | "domainPrefix": "test_api_id", 61 | "requestTimeEpoch": 1669856461000, 62 | "requestId": "a1b2c3d4-5678-90ab-cdef-EXAMPLE11111", 63 | "identity": { 64 | "cognitoIdentityPoolId": null, 65 | "accountId": null, 66 | "cognitoIdentityId": null, 67 | "caller": null, 68 | "sourceIp": "203.0.113.123", 69 | "principalOrgId": null, 70 | "accessKey": null, 71 | "cognitoAuthenticationType": null, 72 | "cognitoAuthenticationProvider": null, 73 | "userArn": null, 74 | "userAgent": "PyTest", 75 | "user": null 76 | }, 77 | "domainName": "test_api_id.execute-api.ap-southeast-1.amazonaws.com", 78 | "apiId": "test_api_id" 79 | }, 80 | "isBase64Encoded": false 81 | } -------------------------------------------------------------------------------- /unicorn_web/tests/unit/events/search_by_city.json: -------------------------------------------------------------------------------- 1 | { 2 | "resource": "/search/{country}/{city}", 3 | "path": "/search/usa/anytown", 4 | "httpMethod": "GET", 5 | "body": null, 6 | "headers": { 7 | "Accept": "*/*", 8 | "Accept-Encoding": "gzip, deflate, br", 9 | "Cache-Control": "no-cache", 10 | "Content-Type": "application/json", 11 | "Host": "test_api_id.execute-api.ap-southeast-1.amazonaws.com", 12 | "User-Agent": "PyTest", 13 | "X-Forwarded-For": "203.0.113.123", 14 | "X-Forwarded-Port": "443", 15 | "X-Forwarded-Proto": "https" 16 | }, 17 | "multiValueHeaders": { 18 | "Accept": [ 19 | "*/*" 20 | ], 21 | "Accept-Encoding": [ 22 | "gzip, deflate, br" 23 | ], 24 | "Cache-Control": [ 25 | "no-cache" 26 | ], 27 | "Content-Type": [ 28 | "application/json" 29 | ], 30 | "Host": [ 31 | "test_api_id.execute-api.ap-southeast-1.amazonaws.com" 32 | ], 33 | "User-Agent": [ 34 | "PyTest" 35 | ], 36 | "X-Forwarded-For": [ 37 | "203.0.113.123" 38 | ], 39 | "X-Forwarded-Port": [ 40 | "443" 41 | ], 42 | "X-Forwarded-Proto": [ 43 | "https" 44 | ] 45 | }, 46 | "queryStringParameters": null, 47 | "multiValueQueryStringParameters": null, 48 | "pathParameters": null, 49 | "stageVariables": null, 50 | "requestContext": { 51 | "resourceId": "resource", 52 | "resourcePath": "/search/{country}/{city}", 53 | "httpMethod": "GET", 54 | "extendedRequestId": "testReqId=", 55 | "requestTime": "01/Dec/2022:01:01:01 +0000", 56 | "path": "/Local/search/usa/anytown", 57 | "accountId": "111122223333", 58 | "protocol": "HTTP/1.1", 59 | "stage": "Local", 60 | "domainPrefix": "test_api_id", 61 | "requestTimeEpoch": 1669856461000, 62 | "requestId": "a1b2c3d4-5678-90ab-cdef-EXAMPLE11111", 63 | "identity": { 64 | "cognitoIdentityPoolId": null, 65 | "accountId": null, 66 | "cognitoIdentityId": null, 67 | "caller": null, 68 | "sourceIp": "203.0.113.123", 69 | "principalOrgId": null, 70 | "accessKey": null, 71 | "cognitoAuthenticationType": null, 72 | "cognitoAuthenticationProvider": null, 73 | "userArn": null, 74 | "userAgent": "PyTest", 75 | "user": null 76 | }, 77 | "domainName": "test_api_id.execute-api.ap-southeast-1.amazonaws.com", 78 | "apiId": "test_api_id" 79 | }, 80 | "isBase64Encoded": false 81 | } -------------------------------------------------------------------------------- /unicorn_web/tests/unit/events/search_by_full_address.json: -------------------------------------------------------------------------------- 1 | { 2 | "resource": "/properties/{country}/{city}/{street}/{number}", 3 | "path": "/properties/usa/anytown/main-street/124", 4 | "httpMethod": "GET", 5 | "body": null, 6 | "headers": { 7 | "Accept": "*/*", 8 | "Accept-Encoding": "gzip, deflate, br", 9 | "Cache-Control": "no-cache", 10 | "Content-Type": "application/json", 11 | "Host": "test_api_id.execute-api.ap-southeast-1.amazonaws.com", 12 | "User-Agent": "PyTest", 13 | "X-Forwarded-For": "203.0.113.123", 14 | "X-Forwarded-Port": "443", 15 | "X-Forwarded-Proto": "https" 16 | }, 17 | "multiValueHeaders": { 18 | "Accept": [ 19 | "*/*" 20 | ], 21 | "Accept-Encoding": [ 22 | "gzip, deflate, br" 23 | ], 24 | "Cache-Control": [ 25 | "no-cache" 26 | ], 27 | "Content-Type": [ 28 | "application/json" 29 | ], 30 | "Host": [ 31 | "test_api_id.execute-api.ap-southeast-1.amazonaws.com" 32 | ], 33 | "User-Agent": [ 34 | "PyTest" 35 | ], 36 | "X-Forwarded-For": [ 37 | "203.0.113.123" 38 | ], 39 | "X-Forwarded-Port": [ 40 | "443" 41 | ], 42 | "X-Forwarded-Proto": [ 43 | "https" 44 | ] 45 | }, 46 | "queryStringParameters": null, 47 | "multiValueQueryStringParameters": null, 48 | "pathParameters": null, 49 | "stageVariables": null, 50 | "requestContext": { 51 | "resourceId": "resource", 52 | "resourcePath": "/properties/{country}/{city}/{street}/{number}", 53 | "httpMethod": "GET", 54 | "extendedRequestId": "testReqId=", 55 | "requestTime": "01/Dec/2022:01:01:01 +0000", 56 | "path": "/Local/properties/usa/anytown/main-street/124", 57 | "accountId": "111122223333", 58 | "protocol": "HTTP/1.1", 59 | "stage": "Local", 60 | "domainPrefix": "test_api_id", 61 | "requestTimeEpoch": 1669856461000, 62 | "requestId": "a1b2c3d4-5678-90ab-cdef-EXAMPLE11111", 63 | "identity": { 64 | "cognitoIdentityPoolId": null, 65 | "accountId": null, 66 | "cognitoIdentityId": null, 67 | "caller": null, 68 | "sourceIp": "203.0.113.123", 69 | "principalOrgId": null, 70 | "accessKey": null, 71 | "cognitoAuthenticationType": null, 72 | "cognitoAuthenticationProvider": null, 73 | "userArn": null, 74 | "userAgent": "PyTest", 75 | "user": null 76 | }, 77 | "domainName": "test_api_id.execute-api.ap-southeast-1.amazonaws.com", 78 | "apiId": "test_api_id" 79 | }, 80 | "isBase64Encoded": false 81 | } -------------------------------------------------------------------------------- /unicorn_web/tests/unit/events/search_by_full_address_declined.json: -------------------------------------------------------------------------------- 1 | { 2 | "resource": "/properties/{country}/{city}/{street}/{number}", 3 | "path": "/properties/usa/anytown/main-street/125", 4 | "httpMethod": "GET", 5 | "body": null, 6 | "headers": { 7 | "Accept": "*/*", 8 | "Accept-Encoding": "gzip, deflate, br", 9 | "Cache-Control": "no-cache", 10 | "Content-Type": "application/json", 11 | "Host": "test_api_id.execute-api.ap-southeast-1.amazonaws.com", 12 | "User-Agent": "PyTest", 13 | "X-Forwarded-For": "203.0.113.123", 14 | "X-Forwarded-Port": "443", 15 | "X-Forwarded-Proto": "https" 16 | }, 17 | "multiValueHeaders": { 18 | "Accept": [ 19 | "*/*" 20 | ], 21 | "Accept-Encoding": [ 22 | "gzip, deflate, br" 23 | ], 24 | "Cache-Control": [ 25 | "no-cache" 26 | ], 27 | "Content-Type": [ 28 | "application/json" 29 | ], 30 | "Host": [ 31 | "test_api_id.execute-api.ap-southeast-1.amazonaws.com" 32 | ], 33 | "User-Agent": [ 34 | "PyTest" 35 | ], 36 | "X-Forwarded-For": [ 37 | "203.0.113.123" 38 | ], 39 | "X-Forwarded-Port": [ 40 | "443" 41 | ], 42 | "X-Forwarded-Proto": [ 43 | "https" 44 | ] 45 | }, 46 | "queryStringParameters": null, 47 | "multiValueQueryStringParameters": null, 48 | "pathParameters": null, 49 | "stageVariables": null, 50 | "requestContext": { 51 | "resourceId": "resource", 52 | "resourcePath": "/properties/{country}/{city}/{street}/{number}", 53 | "httpMethod": "GET", 54 | "extendedRequestId": "testReqId=", 55 | "requestTime": "01/Dec/2022:01:01:01 +0000", 56 | "path": "/Local/properties/usa/anytown/main-street/125", 57 | "accountId": "111122223333", 58 | "protocol": "HTTP/1.1", 59 | "stage": "Local", 60 | "domainPrefix": "test_api_id", 61 | "requestTimeEpoch": 1669856461000, 62 | "requestId": "a1b2c3d4-5678-90ab-cdef-EXAMPLE11111", 63 | "identity": { 64 | "cognitoIdentityPoolId": null, 65 | "accountId": null, 66 | "cognitoIdentityId": null, 67 | "caller": null, 68 | "sourceIp": "203.0.113.123", 69 | "principalOrgId": null, 70 | "accessKey": null, 71 | "cognitoAuthenticationType": null, 72 | "cognitoAuthenticationProvider": null, 73 | "userArn": null, 74 | "userAgent": "PyTest", 75 | "user": null 76 | }, 77 | "domainName": "test_api_id.execute-api.ap-southeast-1.amazonaws.com", 78 | "apiId": "test_api_id" 79 | }, 80 | "isBase64Encoded": false 81 | } -------------------------------------------------------------------------------- /unicorn_web/tests/unit/events/search_by_full_address_new.json: -------------------------------------------------------------------------------- 1 | { 2 | "resource": "/properties/{country}/{city}/{street}/{number}", 3 | "path": "/properties/usa/anytown/main-street/123", 4 | "httpMethod": "GET", 5 | "body": null, 6 | "headers": { 7 | "Accept": "*/*", 8 | "Accept-Encoding": "gzip, deflate, br", 9 | "Cache-Control": "no-cache", 10 | "Content-Type": "application/json", 11 | "Host": "test_api_id.execute-api.ap-southeast-1.amazonaws.com", 12 | "User-Agent": "PyTest", 13 | "X-Forwarded-For": "203.0.113.123", 14 | "X-Forwarded-Port": "443", 15 | "X-Forwarded-Proto": "https" 16 | }, 17 | "multiValueHeaders": { 18 | "Accept": [ 19 | "*/*" 20 | ], 21 | "Accept-Encoding": [ 22 | "gzip, deflate, br" 23 | ], 24 | "Cache-Control": [ 25 | "no-cache" 26 | ], 27 | "Content-Type": [ 28 | "application/json" 29 | ], 30 | "Host": [ 31 | "test_api_id.execute-api.ap-southeast-1.amazonaws.com" 32 | ], 33 | "User-Agent": [ 34 | "PyTest" 35 | ], 36 | "X-Forwarded-For": [ 37 | "203.0.113.123" 38 | ], 39 | "X-Forwarded-Port": [ 40 | "443" 41 | ], 42 | "X-Forwarded-Proto": [ 43 | "https" 44 | ] 45 | }, 46 | "queryStringParameters": null, 47 | "multiValueQueryStringParameters": null, 48 | "pathParameters": null, 49 | "stageVariables": null, 50 | "requestContext": { 51 | "resourceId": "resource", 52 | "resourcePath": "/properties/{country}/{city}/{street}/{number}", 53 | "httpMethod": "GET", 54 | "extendedRequestId": "testReqId=", 55 | "requestTime": "01/Dec/2022:01:01:01 +0000", 56 | "path": "/Local/properties/usa/anytown/main-street/123", 57 | "accountId": "111122223333", 58 | "protocol": "HTTP/1.1", 59 | "stage": "Local", 60 | "domainPrefix": "test_api_id", 61 | "requestTimeEpoch": 1669856461000, 62 | "requestId": "a1b2c3d4-5678-90ab-cdef-EXAMPLE11111", 63 | "identity": { 64 | "cognitoIdentityPoolId": null, 65 | "accountId": null, 66 | "cognitoIdentityId": null, 67 | "caller": null, 68 | "sourceIp": "203.0.113.123", 69 | "principalOrgId": null, 70 | "accessKey": null, 71 | "cognitoAuthenticationType": null, 72 | "cognitoAuthenticationProvider": null, 73 | "userArn": null, 74 | "userAgent": "PyTest", 75 | "user": null 76 | }, 77 | "domainName": "test_api_id.execute-api.ap-southeast-1.amazonaws.com", 78 | "apiId": "test_api_id" 79 | }, 80 | "isBase64Encoded": false 81 | } -------------------------------------------------------------------------------- /unicorn_web/tests/unit/events/search_by_full_address_not_found.json: -------------------------------------------------------------------------------- 1 | { 2 | "resource": "/properties/{country}/{city}/{street}/{number}", 3 | "path": "/properties/usa/anytown/main-street/122", 4 | "httpMethod": "GET", 5 | "body": null, 6 | "headers": { 7 | "Accept": "*/*", 8 | "Accept-Encoding": "gzip, deflate, br", 9 | "Cache-Control": "no-cache", 10 | "Content-Type": "application/json", 11 | "Host": "test_api_id.execute-api.ap-southeast-1.amazonaws.com", 12 | "User-Agent": "PyTest", 13 | "X-Forwarded-For": "203.0.113.123", 14 | "X-Forwarded-Port": "443", 15 | "X-Forwarded-Proto": "https" 16 | }, 17 | "multiValueHeaders": { 18 | "Accept": [ 19 | "*/*" 20 | ], 21 | "Accept-Encoding": [ 22 | "gzip, deflate, br" 23 | ], 24 | "Cache-Control": [ 25 | "no-cache" 26 | ], 27 | "Content-Type": [ 28 | "application/json" 29 | ], 30 | "Host": [ 31 | "test_api_id.execute-api.ap-southeast-1.amazonaws.com" 32 | ], 33 | "User-Agent": [ 34 | "PyTest" 35 | ], 36 | "X-Forwarded-For": [ 37 | "203.0.113.123" 38 | ], 39 | "X-Forwarded-Port": [ 40 | "443" 41 | ], 42 | "X-Forwarded-Proto": [ 43 | "https" 44 | ] 45 | }, 46 | "queryStringParameters": null, 47 | "multiValueQueryStringParameters": null, 48 | "pathParameters": null, 49 | "stageVariables": null, 50 | "requestContext": { 51 | "resourceId": "resource", 52 | "resourcePath": "/properties/{country}/{city}/{street}/{number}", 53 | "httpMethod": "GET", 54 | "extendedRequestId": "testReqId=", 55 | "requestTime": "01/Dec/2022:01:01:01 +0000", 56 | "path": "/Local/properties/usa/anytown/main-street/122", 57 | "accountId": "111122223333", 58 | "protocol": "HTTP/1.1", 59 | "stage": "Local", 60 | "domainPrefix": "test_api_id", 61 | "requestTimeEpoch": 1669856461000, 62 | "requestId": "a1b2c3d4-5678-90ab-cdef-EXAMPLE11111", 63 | "identity": { 64 | "cognitoIdentityPoolId": null, 65 | "accountId": null, 66 | "cognitoIdentityId": null, 67 | "caller": null, 68 | "sourceIp": "203.0.113.123", 69 | "principalOrgId": null, 70 | "accessKey": null, 71 | "cognitoAuthenticationType": null, 72 | "cognitoAuthenticationProvider": null, 73 | "userArn": null, 74 | "userAgent": "PyTest", 75 | "user": null 76 | }, 77 | "domainName": "test_api_id.execute-api.ap-southeast-1.amazonaws.com", 78 | "apiId": "test_api_id" 79 | }, 80 | "isBase64Encoded": false 81 | } -------------------------------------------------------------------------------- /unicorn_web/tests/unit/events/search_by_street_event.json: -------------------------------------------------------------------------------- 1 | { 2 | "resource": "/search/{country}/{city}/{street}", 3 | "path": "/search/usa/anytown/main-street", 4 | "httpMethod": "GET", 5 | "body": null, 6 | "headers": { 7 | "Accept": "*/*", 8 | "Accept-Encoding": "gzip, deflate, br", 9 | "Cache-Control": "no-cache", 10 | "Content-Type": "application/json", 11 | "Host": "test_api_id.execute-api.ap-southeast-1.amazonaws.com", 12 | "User-Agent": "PyTest", 13 | "X-Forwarded-For": "203.0.113.123", 14 | "X-Forwarded-Port": "443", 15 | "X-Forwarded-Proto": "https" 16 | }, 17 | "multiValueHeaders": { 18 | "Accept": [ 19 | "*/*" 20 | ], 21 | "Accept-Encoding": [ 22 | "gzip, deflate, br" 23 | ], 24 | "Cache-Control": [ 25 | "no-cache" 26 | ], 27 | "Content-Type": [ 28 | "application/json" 29 | ], 30 | "Host": [ 31 | "test_api_id.execute-api.ap-southeast-1.amazonaws.com" 32 | ], 33 | "User-Agent": [ 34 | "PyTest" 35 | ], 36 | "X-Forwarded-For": [ 37 | "203.0.113.123" 38 | ], 39 | "X-Forwarded-Port": [ 40 | "443" 41 | ], 42 | "X-Forwarded-Proto": [ 43 | "https" 44 | ] 45 | }, 46 | "queryStringParameters": null, 47 | "multiValueQueryStringParameters": null, 48 | "pathParameters": null, 49 | "stageVariables": null, 50 | "requestContext": { 51 | "resourceId": "resource", 52 | "resourcePath": "/search/{country}/{city}/{street}", 53 | "httpMethod": "GET", 54 | "extendedRequestId": "testReqId=", 55 | "requestTime": "01/Dec/2022:01:01:01 +0000", 56 | "path": "/Local/search/usa/anytown/main-street", 57 | "accountId": "111122223333", 58 | "protocol": "HTTP/1.1", 59 | "stage": "Local", 60 | "domainPrefix": "test_api_id", 61 | "requestTimeEpoch": 1669856461000, 62 | "requestId": "a1b2c3d4-5678-90ab-cdef-EXAMPLE11111", 63 | "identity": { 64 | "cognitoIdentityPoolId": null, 65 | "accountId": null, 66 | "cognitoIdentityId": null, 67 | "caller": null, 68 | "sourceIp": "203.0.113.123", 69 | "principalOrgId": null, 70 | "accessKey": null, 71 | "cognitoAuthenticationType": null, 72 | "cognitoAuthenticationProvider": null, 73 | "userArn": null, 74 | "userAgent": "PyTest", 75 | "user": null 76 | }, 77 | "domainName": "test_api_id.execute-api.ap-southeast-1.amazonaws.com", 78 | "apiId": "test_api_id" 79 | }, 80 | "isBase64Encoded": false 81 | } -------------------------------------------------------------------------------- /unicorn_web/tests/unit/test_publication_approved_event_handler.py: -------------------------------------------------------------------------------- 1 | # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | # SPDX-License-Identifier: MIT-0 3 | 4 | # import os 5 | 6 | # from unittest import mock 7 | # from importlib import reload 8 | 9 | # from .lambda_context import LambdaContext 10 | # from .helper import load_event, return_env_vars_dict, create_ddb_table_property_web 11 | 12 | 13 | # def get_property_pk_sk(property_id): 14 | # country, city, street, number = property_id.split('/') 15 | # pk_details = f"{country}#{city}".replace(' ', '-').lower() 16 | # return { 17 | # 'PK': f"PROPERTY#{pk_details}", 18 | # 'SK': f"{street}#{str(number)}".replace(' ', '-').lower(), 19 | # } 20 | 21 | 22 | # @mock.patch.dict(os.environ, return_env_vars_dict(), clear=True) 23 | # def test_property_approved(dynamodb, mocker): 24 | # eventbridge_event = load_event('events/property_approved.json') 25 | # property_id = eventbridge_event['detail']['property_id'] 26 | 27 | # import approvals_service.publication_approved_event_handler as app 28 | # reload(app) # Reload is required to prevent function setup reuse from another test 29 | 30 | # create_ddb_table_property_web(dynamodb) 31 | 32 | # ret = app.lambda_handler(eventbridge_event, LambdaContext()) # type: ignore 33 | # assert ret['result'] == 'Successfully updated property status' 34 | 35 | # ddbitem_after = dynamodb.Table('table1').get_item(Key=get_property_pk_sk(property_id)) 36 | # assert ddbitem_after['Item']['status'] == 'APPROVED' 37 | --------------------------------------------------------------------------------