Secure Python Customer Plan Lookup
Build and test a secure Python customer plan lookup with parameterized MySQL.
Introduction
30 Second Summary
A customer should be able to check their phone plan without exposing private details. A careless shortcut can turn that simple request into a security problem.
In this project, you will rebuild a secure customer plan lookup in Python with the same two-argument contract as an AWS Lambda handler. A local test suite proves that customer data reaches the database through parameterized SQL.
What You'll Build
When the finished tests run, you will see a sample mobile number return its plan through a secure handler.
By the end of this project, you'll have:
- A local customer plan demo where lambda_handler(event, context) returns Everyday 20GB for +61400000000.
- A strict mobile-number gate that blocks malformed input before database access.
- Automated proof of SQL parameter separation plus connection cleanup across successful and failed lookups.
- Secret Mission: Add a privacy regression guard that fails if logs expose a mobile number or raw database error.
What do I need for this project?
This project expects a Mac with Visual Studio Code plus Python 3.12.
Setup installs PyMySQL 1.2.0 locally without creating AWS resources.
Before We Start
This project begins with one clear boundary: you are proving the customer-plan lookup contract locally. Private RDS, RDS Proxy, Secrets Manager, and CloudFormation remain for the next project.
Set Up the Local Replica
The security checks in later steps need a small local project that runs without the larger AWS deployment surface. A reproducible skeleton keeps every test focused on application behavior.
You'll use Visual Studio Code to build the skeleton. Python 3.12 then runs the local checks.
In this step, get ready to:
- Create the local project folders and files.
- Verify Python and install PyMySQL from the dependency file.
- Prove that test discovery works with a smoke test.
Create the project skeleton
The project skeleton separates application code in src/ from automated checks in tests/. The empty __init__.py files make both folders importable Python packages.
- Press Command-Space on your Mac to open Spotlight.
You'll see the Spotlight search field.
- Type Visual Studio Code into Spotlight.
- Press Return to open Visual Studio Code.
Visual Studio Code opens with an empty workspace or your previous workspace.
- Click File in the macOS menu bar.
- Select Open Folder....
You'll see the macOS folder picker.
- Select Desktop in the folder picker.
- Select New Folder.
- Enter customer-plan-lookup-core as the folder name.
- Press Return to create the folder.
Because you created this folder locally, you can safely confirm a workspace trust prompt if one appears.
- Select the new customer-plan-lookup-core folder.
- Click Open.
- Confirm that you trust the folder if Visual Studio Code asks.
Your project now has a home. You'll see customer-plan-lookup-core at the top of the Explorer sidebar.
- Use the New Folder... button in the Explorer sidebar to create src/.
- Use the New Folder... button to create tests/.
You'll see both folders under customer-plan-lookup-core in the Explorer sidebar.
- Use the New File... button to create requirements.txt inside customer-plan-lookup-core.
- Use the New File... button to create src/__init__.py.
- Use the New File... button to create tests/__init__.py.
- Use the New File... button to create tests/test_smoke.py.
You should now see requirements.txt beside the src/ and tests/ folders. Each package contains its empty __init__.py file.
Verify Python and install PyMySQL
The project uses Python 3.12 so every local result comes from the expected interpreter. PyMySQL 1.2.0 supplies the database cursor contract that later steps reproduce with fakes.
- Click View in the Visual Studio Code menu bar.
- Select Terminal.
- Check your installed Python version by running this command:
python3 --version
What does this command do?
The python3 --version command prints the Python version used by this Terminal session. This confirms the interpreter before any dependency is installed.
✔️ I see Python 3.12
Your Terminal shows Python 3.12. The interpreter is ready for this project.
ⓧ I see an older version
The existing Python installation is below the version used by this project. Install a release from the 3.12 series before continuing.
- Visit the official Python releases for macOS page.
- Download a macOS installer from the Python 3.12 series.
- Run the downloaded installer.
- Switch back to Visual Studio Code after installation finishes.
- Return to the Terminal panel from earlier.
- Repeat the version check above.
ⓧ Command not found
Your Terminal cannot find a Python interpreter yet. Install Python 3.12 from the official macOS releases page.
- Visit the official Python releases for macOS page.
- Download a macOS installer from the Python 3.12 series.
- Run the downloaded installer.
- Switch back to Visual Studio Code after installation finishes.
- Return to the Terminal panel from earlier.
- Repeat the version check above.
- Select requirements.txt in the Explorer sidebar.
- Set the exact dependency pin by adding this line:
PyMySQL==1.2.0
Why pin this version?
The PyMySQL==1.2.0 pin gives every learner the same package behavior. Later tests can rely on one stable cursor contract.
- Save requirements.txt by pressing Command-S.
- Install the dependency listed in requirements.txt by running:
python3 -m pip install -r requirements.txt
What does this command do?
Python runs pip through the verified interpreter. The installer reads requirements.txt and installs its pinned package.
The Terminal should return to its prompt without an installation error. Your local project now has the exact PyMySQL release it needs.
Dependency installation failed?
- Check that the Terminal is running inside the customer-plan-lookup-core folder.
- Check that requirements.txt contains the dependency line exactly as shown above.
- Confirm that your internet connection is available before trying the installation again.
Ask for help with the installation details: diagnose my PyMySQL installation failure
Add and run the smoke test
A smoke test answers one narrow question. It proves that Python can discover and execute tests from this project before the handler code exists.
- Select tests/test_smoke.py in the Explorer sidebar.
- Add the smoke test by copying this code into the file:
from unittest import TestCase
class SmokeTest(TestCase):
def test_python_test_runner_is_working(self):
self.assertTrue(True)
What does this test prove?
- TestCase provides Python's built-in test behavior.
- test_python_test_runner_is_working starts with the naming pattern used by test discovery.
- self.assertTrue(True) creates a simple passing assertion that proves the runner executed the method.
- Save tests/test_smoke.py by pressing Command-S.
Before you run the suite, do you think Python will discover the test inside the tests/ package?
- Run test discovery from the project folder by running:
python3 -m unittest discover
What does test discovery do?
Python's unittest runner searches for importable test modules from the project folder. It then executes methods whose names use the test naming pattern.
You should see one test run followed by OK.
That's your first green check. The project can now discover and run automated tests from its local folder structure.
Smoke test not passing?
- Confirm that the Terminal prompt is inside customer-plan-lookup-core.
- Confirm that the test file is named tests/test_smoke.py.
- Confirm that both src/__init__.py and tests/__init__.py exist.
Get help tracing the discovery problem: help me fix my smoke test discovery
✔️ Awesome, I've got everything!
Great. Double check that every file is saved and the test run ends with OK.
ⓧ I'd like to double check the full code
Compare the non-empty files below with your project. Keep src/__init__.py and tests/__init__.py completely empty.
PyMySQL==1.2.0
What belongs in this file?
The dependency file contains one exact pin. No other packages are needed in this step.
from unittest import TestCase
class SmokeTest(TestCase):
def test_python_test_runner_is_working(self):
self.assertTrue(True)
What belongs in this file?
The smoke test contains one test class with one passing assertion. This is the complete test file at this point.
Your local replica is ready for application code. Next up, you'll return the first visible customer plan from a Lambda-shaped handler.
Return the First Customer Plan
Your project skeleton can already discover its smoke test. It still has no customer-plan response to prove the lookup contract.
Before database behavior adds complexity, you need the smallest working version of the promised lookup. You’ll create a local handler with the same two-argument contract as AWS Lambda.
In this step, get ready to:
- Create a Lambda-shaped handler.
- Return a sample mobile number plus its plan.
- Add a happy-path test for the response.
Create the first handler response
An AWS Lambda handler accepts event plus context arguments. Its response must be serializable as JSON.
- In the Visual Studio Code window from setup, create src/lambda_function.py with the file creation icon above the left file tree.
- Build the first customer response by adding this code to src/lambda_function.py:
def lambda_handler(event, context):
del event
del context
return {
"statusCode": 200,
"body": {
"mobile_number": "+61400000000",
"plan": "Everyday 20GB",
},
}
What does this code do?
- The lambda_handler(event, context) function keeps the two-argument shape used by AWS Lambda.
- The del event line makes this temporary response independent of request data.
- The del context line makes this temporary response independent of runtime metadata.
- The statusCode value records a successful 200 outcome.
- The body contains only the sample mobile_number plus its plan.
- Save src/lambda_function.py.
- Confirm the left file tree now lists lambda_function.py inside src.
Can’t find the handler file?
- Move lambda_function.py into src if it appears beside the folder.
- Compare the indentation inside lambda_handler(event, context) with the code above.
Help me troubleshoot why my handler file is missing or misplaced.
✔️ Awesome, I've got everything!
Good progress. Your local Lambda contract now returns a complete sample response.
ⓧ I'd like to double check the full code
The complete src/lambda_function.py file should look like this.
def lambda_handler(event, context):
del event
del context
return {
"statusCode": 200,
"body": {
"mobile_number": "+61400000000",
"plan": "Everyday 20GB",
},
}
Test the customer plan response
A unit test turns the sample response into an executable contract. It checks the returned status plus the exact body.
- In the Visual Studio Code window from setup, create tests/test_lambda_function.py with the file creation icon above the left file tree.
- Add the happy-path test to tests/test_lambda_function.py with this code:
from unittest import TestCase
from src.lambda_function import lambda_handler
class CustomerPlanLookupTests(TestCase):
def test_returns_customer_plan(self):
response = lambda_handler({"mobile_number": "+61400000000"}, None)
self.assertEqual(response["statusCode"], 200)
self.assertEqual(
response["body"],
{
"mobile_number": "+61400000000",
"plan": "Everyday 20GB",
},
)
What does this test prove?
- The import loads lambda_handler from the source package.
- The test calls the handler with +61400000000 as request data.
- The first assertion checks for the successful 200 status.
- The second assertion checks the complete response body for the expected mobile_number plus the expected plan.
- Save tests/test_lambda_function.py.
Before you run the suite, do you expect Python to discover only the new handler test or both test files?
- Switch back to the terminal from the setup step.
- Run the full local suite from the customer-plan-lookup-core folder by running this command:
python3 -m unittest discover
What does this check prove?
- The unittest discovery process loads tests from both test files.
- The smoke test confirms the test runner still works.
- The handler test confirms the response matches the customer-plan contract.
You should see two tests complete followed by OK.
That is your first working lookup slice. The passing assertions prove the response contains +61400000000 with the Everyday 20GB plan.
Are the tests failing?
- Check that lambda_function.py sits inside src.
- Check that test_lambda_function.py sits inside tests.
- Compare the response keys plus values with the test expectations above.
Help me diagnose why my customer plan test is failing.
✔️ Awesome, I've got everything!
Your handler test is saved. The smoke test plus customer-plan test now pass together.
ⓧ I'd like to double check the full code
The complete tests/test_lambda_function.py file should look like this.
from unittest import TestCase
from src.lambda_function import lambda_handler
class CustomerPlanLookupTests(TestCase):
def test_returns_customer_plan(self):
response = lambda_handler({"mobile_number": "+61400000000"}, None)
self.assertEqual(response["statusCode"], 200)
self.assertEqual(
response["body"],
{
"mobile_number": "+61400000000",
"plan": "Everyday 20GB",
},
)
Your handler now returns a tested customer plan from local code. Next, you’ll connect it to a fake cursor to reveal the first database security gap.
Expose the Unsafe SQL Shortcut
Your handler already proves the AWS Lambda-shaped response works locally. The next gap is the database boundary.
Placing a mobile number directly inside SQL feels convenient. A query-shape test will expose the security defect before you repair it.
In this step, get ready to:
- Model the cursor contract with local test doubles.
- Route the handler through an interpolated SQL lookup.
- Add a contract test that exposes the embedded customer value.
Build the fake cursor contract
A fake MySQL cursor lets your tests record each query without contacting a database. The recorded call makes the query shape visible.
- In the VS Code Explorer sidebar, select tests/test_lambda_function.py.
- Replace the imports at the top of tests/test_lambda_function.py with this fixture block:
from unittest import TestCase
from unittest.mock import patch
from src.lambda_function import lambda_handler
MOBILE_NUMBER = "+61400000000"
CUSTOMER_ROW = {
"mobile_number": MOBILE_NUMBER,
"plan": "Everyday 20GB",
}
What does this fixture block do?
- The patch import lets the test substitute a fake connection for the runtime connection factory.
- MOBILE_NUMBER gives every test one shared customer value.
- CUSTOMER_ROW represents the dictionary returned by the fake cursor.
- Insert the fake cursor below CUSTOMER_ROW by pasting this class:
class FakeCursor:
def __init__(self, row=None):
self.row = row
self.executions = []
def execute(self, query, args=None):
self.executions.append((query, args))
def fetchone(self):
return self.row
def __enter__(self):
return self
def __exit__(self, exc_type, exc_value, traceback):
return False
How does the fake cursor help?
- executions stores every query together with the arguments supplied to execute().
- fetchone() returns the customer row chosen by the test.
- __enter__() supports the cursor context used by the application code.
- __exit__() lets the context finish without suppressing errors.
- Save tests/test_lambda_function.py.
- Confirm the existing tests still pass by running this command in the VS Code terminal:
python3 -m unittest discover
What does this check prove?
The command proves the new fixtures load without disrupting test discovery. The fake cursor is ready for the database-shaped lookup.
You should see two tests run followed by OK.
Seeing an import or syntax problem?
- Check that both import lines remain above MOBILE_NUMBER.
- Check that every method inside FakeCursor uses four spaces of indentation.
- Compare the quotes around +61400000000 with the fixture block.
Ask for help with the test fixture:
Route the handler through the fake connection
The cursor needs a connection object that hands it to the lookup. The connection also records whether the application closes it.
- Insert FakeConnection below FakeCursor by pasting this class:
class FakeConnection:
def __init__(self, cursor):
self.fake_cursor = cursor
self.closed = False
def cursor(self):
return self.fake_cursor
def close(self):
self.closed = True
What does the fake connection track?
- cursor() returns the fake cursor configured by the test.
- closed starts as False so the test can detect cleanup.
- close() changes that state to True.
- Switch to src/lambda_function.py in the VS Code Explorer sidebar.
- Insert these lookup functions above the existing lambda_handler() function:
def connection_factory():
raise RuntimeError("Configure a database connection in the next step")
def find_customer_plan(mobile_number, factory):
connection = factory()
try:
with connection.cursor() as cursor:
query = (
"SELECT mobile_number, plan "
"FROM customers "
f"WHERE mobile_number = '{mobile_number}'"
)
cursor.execute(query)
row = cursor.fetchone()
finally:
connection.close()
return row
Where is the unsafe shortcut?
- connection_factory() marks the boundary where a runtime database connection belongs.
- find_customer_plan() obtains a connection from the supplied factory.
- The formatted string inserts mobile_number directly into the SQL statement.
- The finally block closes the connection after the lookup.
- Save src/lambda_function.py.
- Confirm the original handler behavior still passes by running the suite:
python3 -m unittest discover
What does this run establish?
The existing handler still returns its temporary customer dictionary. The new lookup functions are now available for the next test update.
You should still see two tests run followed by OK.
The happy-path test now needs to supply the fake connection. The updated handler can then retrieve its response body from the fake cursor.
- Return to tests/test_lambda_function.py.
- Replace the current test_returns_customer_plan() method with this version:
def test_returns_customer_plan(self):
cursor = FakeCursor(row=CUSTOMER_ROW)
connection = FakeConnection(cursor)
with patch(
"src.lambda_function.connection_factory",
return_value=connection,
):
response = lambda_handler({"mobile_number": MOBILE_NUMBER}, None)
self.assertEqual(response["statusCode"], 200)
self.assertEqual(response["body"], CUSTOMER_ROW)
self.assertTrue(connection.closed)
How does the happy-path test work?
- FakeCursor receives the sample customer row.
- patch() makes connection_factory() return the fake connection during the test.
- The response assertions preserve the existing customer-plan contract.
- The final assertion proves the handler closes the connection.
- Switch back to src/lambda_function.py.
- Replace the existing lambda_handler() function with this version:
def lambda_handler(event, context):
del context
customer = find_customer_plan(event["mobile_number"], connection_factory)
return {
"statusCode": 200,
"body": customer,
}
What changed in the handler?
- lambda_handler() reads the mobile number from the event.
- find_customer_plan() now supplies the response body from the cursor row.
- The response keeps the same statusCode plus body structure.
- Save src/lambda_function.py.
- Save tests/test_lambda_function.py.
- Verify the fake database path by running the test suite:
python3 -m unittest discover
What does this successful run prove?
The handler now reaches the fake cursor through the connection factory. The customer row becomes the response body without requiring a live database.
You should see two tests run followed by OK. The customer-plan test also confirms that the fake connection closes.
Is the customer-plan test failing?
- Check that the patch target is exactly src.lambda_function.connection_factory.
- Check that lambda_handler() passes connection_factory into find_customer_plan().
- Check that connection.close() remains inside the finally block.
Help me trace the fake connection path:
Make the unsafe query fail its contract
A secure query shape keeps customer data separate from the statement text. The next test records both cursor arguments so the shortcut has nowhere to hide.
- In tests/test_lambda_function.py, add this method below test_returns_customer_plan() inside CustomerPlanLookupTests:
def test_keeps_sql_and_mobile_number_separate(self):
cursor = FakeCursor(row=CUSTOMER_ROW)
connection = FakeConnection(cursor)
with patch(
"src.lambda_function.connection_factory",
return_value=connection,
):
lambda_handler({"mobile_number": MOBILE_NUMBER}, None)
self.assertEqual(
cursor.executions,
[(
"SELECT mobile_number, plan FROM customers WHERE mobile_number = %s",
(MOBILE_NUMBER,),
)],
)
What does the query-shape test demand?
- The expected SQL contains a stable %s placeholder.
- The expected argument is a one-item tuple containing MOBILE_NUMBER.
- cursor.executions reveals whether the lookup supplied these values separately.
- Save tests/test_lambda_function.py.
Before you run the suite, do you think the new contract check will pass or fail against the interpolated query?
- Reveal the query shape by running the full test suite:
python3 -m unittest discover
What does this run compare?
The test compares the cursor call against a stable SQL statement plus a separate parameter tuple. This turns the unsafe shortcut into visible evidence.
You'll see one test fail. The cursor recorded the value +61400000000 inside the SQL statement.
Its separate argument slot is None. This intentional failure proves the shortcut breaks the required query boundary.
This failure is the result
The handler still returns the customer plan because the fake cursor accepts the interpolated query. The failing assertion catches the security defect that the happy-path test cannot see.
Did the intended failure look different?
- Check that test_keeps_sql_and_mobile_number_separate() is indented inside CustomerPlanLookupTests.
- Check that the expected query contains %s.
- Check that find_customer_plan() still calls cursor.execute(query) with one argument.
Help me inspect the intentional test failure:
✔️ Awesome, I've exposed the shortcut
You have a reproducible test that exposes the embedded mobile number. Keep the deliberate failure in place for the next step.
ⓧ I'd like to double check the full code
Compare your complete src/lambda_function.py file with this version:
def connection_factory():
raise RuntimeError("Configure a database connection in the next step")
def find_customer_plan(mobile_number, factory):
connection = factory()
try:
with connection.cursor() as cursor:
query = (
"SELECT mobile_number, plan "
"FROM customers "
f"WHERE mobile_number = '{mobile_number}'"
)
cursor.execute(query)
row = cursor.fetchone()
finally:
connection.close()
return row
def lambda_handler(event, context):
del context
customer = find_customer_plan(event["mobile_number"], connection_factory)
return {
"statusCode": 200,
"body": customer,
}
What to compare in the handler
Confirm that the query still embeds mobile_number through the formatted string. Confirm that cursor.execute(query) receives no separate parameter tuple.
Compare your complete tests/test_lambda_function.py file with this version:
from unittest import TestCase
from unittest.mock import patch
from src.lambda_function import lambda_handler
MOBILE_NUMBER = "+61400000000"
CUSTOMER_ROW = {
"mobile_number": MOBILE_NUMBER,
"plan": "Everyday 20GB",
}
class FakeCursor:
def __init__(self, row=None):
self.row = row
self.executions = []
def execute(self, query, args=None):
self.executions.append((query, args))
def fetchone(self):
return self.row
def __enter__(self):
return self
def __exit__(self, exc_type, exc_value, traceback):
return False
class FakeConnection:
def __init__(self, cursor):
self.fake_cursor = cursor
self.closed = False
def cursor(self):
return self.fake_cursor
def close(self):
self.closed = True
class CustomerPlanLookupTests(TestCase):
def test_returns_customer_plan(self):
cursor = FakeCursor(row=CUSTOMER_ROW)
connection = FakeConnection(cursor)
with patch(
"src.lambda_function.connection_factory",
return_value=connection,
):
response = lambda_handler({"mobile_number": MOBILE_NUMBER}, None)
self.assertEqual(response["statusCode"], 200)
self.assertEqual(response["body"], CUSTOMER_ROW)
self.assertTrue(connection.closed)
def test_keeps_sql_and_mobile_number_separate(self):
cursor = FakeCursor(row=CUSTOMER_ROW)
connection = FakeConnection(cursor)
with patch(
"src.lambda_function.connection_factory",
return_value=connection,
):
lambda_handler({"mobile_number": MOBILE_NUMBER}, None)
self.assertEqual(
cursor.executions,
[(
"SELECT mobile_number, plan FROM customers WHERE mobile_number = %s",
(MOBILE_NUMBER,),
)],
)
What to compare in the tests
Confirm that FakeCursor records both cursor arguments. Confirm that the final test expects the SQL statement plus the mobile-number tuple as separate values.
You have made the unsafe query shape impossible to overlook. Next, you'll use PyMySQL parameter binding to make the failing contract pass.
Separate SQL from Customer Data
Your deliberate failing test exposed the exact weakness from the last step. Customer input was becoming part of the SQL text.
PyMySQL parameter binding keeps the statement shape stable. This step moves the mobile number into a separate parameter tuple.
In this step, get ready to:
- Replace SQL interpolation with a stable statement and a separate parameter tuple.
- Add the real PyMySQL connection builder behind the test boundary.
- Return mapped customer data while preserving connection cleanup.
Bind the Mobile Number as Data
A parameterized query contains a placeholder for customer data. The database driver receives the statement separately from the value that fills that placeholder.
- In the Explorer sidebar in Visual Studio Code, select src/lambda_function.py.
- Add the imports and query constants at the top of src/lambda_function.py by pasting this code before def connection_factory()::
import pymysql
import pymysql.cursors
LOOKUP_SQL = (
"SELECT mobile_number, plan "
"FROM customers "
"WHERE mobile_number = %s"
)
DATABASE_CONFIG = None
What Does This Code Do?
- The PyMySQL imports provide the connection function and dictionary cursor used by the production boundary.
- LOOKUP_SQL defines one stable statement with a %s placeholder for the mobile number.
- DATABASE_CONFIG keeps runtime database configuration outside this local test flow.
- In find_customer_plan(), remove the block beginning with query = ( and ending with cursor.execute(query).
- Replace that block with the parameterized cursor call by pasting this code:
cursor.execute(LOOKUP_SQL, (mobile_number,))
row = cursor.fetchone()
Why This Fix Works
- cursor.execute() receives LOOKUP_SQL as the stable statement.
- (mobile_number,) is a one-item tuple containing the customer value.
- The comma inside the tuple is required because Python uses it to distinguish a one-item tuple from a grouped expression.
- Save src/lambda_function.py.
Before you run the suite, predict whether the stable query shape will satisfy the test that failed earlier.
- Test the parameter separation by running this command in the Visual Studio Code terminal:
python3 -m unittest discover
What Should You See?
You should see all three tests pass. The query-shape test now observes a stable statement plus a separate tuple containing the mobile number.
Is the Query-Shape Test Still Failing?
- Check that LOOKUP_SQL ends with the %s placeholder.
- Check that cursor.execute() receives two arguments.
- Check that the second argument is written as (mobile_number,) with the trailing comma.
Help me diagnose the parameter-separation test.
The production code now has one source of truth for the query. The test can import that constant while its fake cursor continues recording the two arguments.
- In the Explorer sidebar, select tests/test_lambda_function.py.
- Find the import from src.lambda_function.
- Replace that import with this line:
from src.lambda_function import LOOKUP_SQL, lambda_handler
Why Import the Query Constant?
The test now refers to the same stable statement used by the lookup. A later query change has one named location in the production module.
- Find the execute() method inside FakeCursor.
- Replace the method with this version:
def execute(self, query, args=None):
self.executions.append((query, args))
return 1
What Does the Fake Cursor Record?
The fake cursor stores each statement beside its parameter argument. Its return value mirrors a successful cursor execution without contacting a database.
- Save tests/test_lambda_function.py.
- Confirm the fake cursor still supports the tests by running this command:
python3 -m unittest discover
What Should You See?
You should see the test suite pass again. The fake cursor now records each execution and returns a successful result count.
- In test_keeps_sql_and_mobile_number_separate(), find the expected cursor.executions value.
- Replace the expected value with this named-query version:
[(LOOKUP_SQL, (MOBILE_NUMBER,))],
What Does This Assertion Prove?
The assertion requires the cursor to receive the stable query and customer value as separate arguments. Direct interpolation changes that recorded shape and makes the test fail.
- Save tests/test_lambda_function.py.
- Verify the updated query contract by running this command:
python3 -m unittest discover
What Should You See?
You should see every test pass. The query-shape assertion now uses LOOKUP_SQL directly.
Did the Test File Stop Importing?
- Check that the import includes both LOOKUP_SQL and lambda_handler.
- Check that the expected tuple contains MOBILE_NUMBER.
- Check that every opening parenthesis in the assertion has a matching closing parenthesis.
Help me fix the updated query-shape test.
Build the PyMySQL Connection Boundary
The tests replace connection_factory() with a fake connection. This keeps local tests deterministic while the production module retains a real connection builder.
- Switch back to src/lambda_function.py in Visual Studio Code.
- Add the production connection builder below DATABASE_CONFIG = None by pasting this code:
def build_connection(config):
return pymysql.connect(
host=config["host"],
user=config["username"],
password=config["password"],
database=config["database"],
port=config["port"],
connect_timeout=config["connect_timeout"],
read_timeout=config["read_timeout"],
write_timeout=config["write_timeout"],
cursorclass=pymysql.cursors.DictCursor,
)
What Does the Connection Builder Do?
- build_connection() passes the supplied database settings to pymysql.connect.
- The timeout values place connection and query limits at the database boundary.
- pymysql.cursors.DictCursor makes each selected row available through keys such as mobile_number and plan.
- Save src/lambda_function.py.
- Confirm the new builder preserves the tested behavior by running this command:
python3 -m unittest discover
What Should You See?
You should see every test pass. The tests continue using their fake connection because they patch the production factory.
Is PyMySQL Missing?
- Check that the terminal is still inside the customer-plan-lookup-core folder.
- Check that the PyMySQL imports remain at the top of src/lambda_function.py.
- Check that the dependency installation from the setup step completed successfully.
Help me diagnose the PyMySQL import failure.
- Find the existing connection_factory() function in src/lambda_function.py.
- Replace that function with this configuration boundary:
def connection_factory():
if DATABASE_CONFIG is None:
raise RuntimeError("Database configuration is not available")
return build_connection(DATABASE_CONFIG)
Why Keep a Factory Boundary?
The factory separates connection creation from lookup behavior. Tests can replace it with FakeConnection without changing the handler.
- Save src/lambda_function.py.
- Verify the connection boundary with the fake database by running this command:
python3 -m unittest discover
What Should You See?
You should see the full suite pass without a real database connection. The patched factory keeps database configuration outside the local test path.
Is the Configuration Error Reaching the Test?
- Check that each handler test patches src.lambda_function.connection_factory.
- Check that the patch returns the test's FakeConnection object.
Help me fix the connection factory patch.
Return Controlled Lookup Results
A dictionary cursor supplies named fields from the selected row. The lookup can copy only the mobile number and plan into its return value.
- In find_customer_plan(), find the existing return row line.
- Replace that line with this result mapping:
if row is None:
return None
return {
"mobile_number": row["mobile_number"],
"plan": row["plan"],
}
What Does the Result Mapping Do?
- A missing row becomes None so the handler can return a controlled not-found response.
- A found row becomes a new dictionary containing only mobile_number and plan.
- Save src/lambda_function.py.
- Verify the fake customer row still passes through the lookup by running this command:
python3 -m unittest discover
What Should You See?
You should see the customer-plan test pass. Its response body still contains the sample mobile number and the Everyday 20GB plan.
Is the Customer Plan Test Failing?
- Check that the result dictionary reads both values from row.
- Check that the dictionary keys match mobile_number and plan exactly.
Help me fix the customer result mapping.
Your existing finally block still closes the connection after the cursor work. The passing happy-path test proves that cleanup remains active.
- Add the response helper above lambda_handler() by pasting this code:
def make_response(status_code, body):
return {
"statusCode": status_code,
"body": body,
}
What Does the Response Helper Do?
make_response() gives every handler result the same JSON-compatible shape. It keeps the status code beside its response body.
- Find the existing lambda_handler() function.
- Replace the full handler with this version:
def lambda_handler(event, context):
del context
customer = find_customer_plan(event["mobile_number"], connection_factory)
if customer is None:
return make_response(404, {"message": "Customer not found"})
return make_response(200, customer)
How Does the Handler Choose a Response?
- The handler passes the event's mobile number to find_customer_plan().
- A missing customer produces a controlled 404 response.
- A found customer produces a 200 response containing the mapped row.
- Save src/lambda_function.py.
Before you run the final check, predict what the cursor should record for the query and mobile number.
- Run the complete test suite from the customer-plan-lookup-core folder with this command:
python3 -m unittest discover
What Should You See?
You should see every test pass. The query-shape test records LOOKUP_SQL and (+61400000000,) as separate cursor arguments.
The happy-path response contains +61400000000 with the sample plan. Its fake connection also ends with closed set to True.
Are Any Tests Still Failing?
- Check that cursor.execute() receives LOOKUP_SQL as its first argument.
- Check that the customer value remains inside (mobile_number,).
- Check that connection.close() remains inside the finally block.
Help me diagnose the final parameterized lookup tests.
That security gap is closed. Customer input now travels beside the SQL statement while the connection still closes after each lookup.
Use these references to confirm both edited files match the completed step.
✔️ Awesome, I've got everything!
Great work. Your src/lambda_function.py file now contains the parameterized lookup and production connection boundary.
ⓧ I'd like to double check the full code
import pymysql
import pymysql.cursors
LOOKUP_SQL = (
"SELECT mobile_number, plan "
"FROM customers "
"WHERE mobile_number = %s"
)
DATABASE_CONFIG = None
def build_connection(config):
return pymysql.connect(
host=config["host"],
user=config["username"],
password=config["password"],
database=config["database"],
port=config["port"],
connect_timeout=config["connect_timeout"],
read_timeout=config["read_timeout"],
write_timeout=config["write_timeout"],
cursorclass=pymysql.cursors.DictCursor,
)
def connection_factory():
if DATABASE_CONFIG is None:
raise RuntimeError("Database configuration is not available")
return build_connection(DATABASE_CONFIG)
def find_customer_plan(mobile_number, factory):
connection = factory()
try:
with connection.cursor() as cursor:
cursor.execute(LOOKUP_SQL, (mobile_number,))
row = cursor.fetchone()
finally:
connection.close()
if row is None:
return None
return {
"mobile_number": row["mobile_number"],
"plan": row["plan"],
}
def make_response(status_code, body):
return {
"statusCode": status_code,
"body": body,
}
def lambda_handler(event, context):
del context
customer = find_customer_plan(event["mobile_number"], connection_factory)
if customer is None:
return make_response(404, {"message": "Customer not found"})
return make_response(200, customer)
How to Use This Reference
This reference shows the complete production module for this point in the project. Its query placeholder and parameter tuple remain separate inside find_customer_plan().
✔️ My tests match too
Your fake cursor and query-shape assertion now match the production contract.
ⓧ I'd like to double check the test code
from unittest import TestCase
from unittest.mock import patch
from src.lambda_function import LOOKUP_SQL, lambda_handler
MOBILE_NUMBER = "+61400000000"
CUSTOMER_ROW = {
"mobile_number": MOBILE_NUMBER,
"plan": "Everyday 20GB",
}
class FakeCursor:
def __init__(self, row=None):
self.row = row
self.executions = []
def execute(self, query, args=None):
self.executions.append((query, args))
return 1
def fetchone(self):
return self.row
def __enter__(self):
return self
def __exit__(self, exc_type, exc_value, traceback):
return False
class FakeConnection:
def __init__(self, cursor):
self.fake_cursor = cursor
self.closed = False
def cursor(self):
return self.fake_cursor
def close(self):
self.closed = True
class CustomerPlanLookupTests(TestCase):
def test_returns_customer_plan(self):
cursor = FakeCursor(row=CUSTOMER_ROW)
connection = FakeConnection(cursor)
with patch(
"src.lambda_function.connection_factory",
return_value=connection,
):
response = lambda_handler({"mobile_number": MOBILE_NUMBER}, None)
self.assertEqual(response["statusCode"], 200)
self.assertEqual(response["body"], CUSTOMER_ROW)
self.assertTrue(connection.closed)
def test_keeps_sql_and_mobile_number_separate(self):
cursor = FakeCursor(row=CUSTOMER_ROW)
connection = FakeConnection(cursor)
with patch(
"src.lambda_function.connection_factory",
return_value=connection,
):
lambda_handler({"mobile_number": MOBILE_NUMBER}, None)
self.assertEqual(
cursor.executions,
[(LOOKUP_SQL, (MOBILE_NUMBER,))],
)
How to Use This Reference
This reference shows the complete handler test file for this step. The fake cursor records the same query and argument shape expected from PyMySQL.
Your lookup now keeps customer data separate from its SQL statement. Next, you will harden the handler against malformed input and database failures.
Harden the Lookup Contract
Your AWS Lambda-shaped handler now keeps the mobile number separate from its SQL statement. That closes the query-shape gap you exposed earlier.
The handler still needs predictable behavior for malformed input. It also needs controlled outcomes when a customer is missing or the database fails.
In this step, get ready to:
- Reject malformed mobile numbers before database access.
- Return controlled responses for lookup failures.
- Prove the hardened contract with automated tests.
Reject malformed mobile numbers
A full-string regular expression gives the handler one accepted mobile-number shape. The value must begin with +61 followed by exactly nine ASCII digits.
- In the Visual Studio Code Explorer sidebar, select src/lambda_function.py.
- Find the current import block at the top of the file:
import pymysql
import pymysql.cursors
What does this code do?
These imports provide the existing database connection classes. The new input gate also needs Python's regular-expression module.
- Replace the current import block with this updated block:
import re
import pymysql
import pymysql.cursors
What changed?
The re import lets the module compile a reusable input pattern. The database imports remain unchanged.
- Add the mobile-number pattern directly above LOOKUP_SQL using this line:
MOBILE_PATTERN = re.compile(r"\+61[0-9]{9}\Z")
How does the pattern work?
- The escaped plus sign requires the value to begin with a literal + character.
- The [0-9]{9} section requires exactly nine ASCII digits after 61.
- The \Z ending prevents extra characters from appearing after the final digit.
- Add validate_mobile_number() directly above find_customer_plan() using this code:
def validate_mobile_number(value):
if not isinstance(value, str) or MOBILE_PATTERN.fullmatch(value) is None:
raise ValueError("mobile_number must be +61 followed by 9 digits")
return value
What does this validation do?
- The type check rejects missing values plus non-string values.
- The fullmatch() check requires the entire string to match MOBILE_PATTERN.
- The function returns the approved string for the database lookup.
- In lambda_handler(), add this input gate directly below del context:
if not isinstance(event, dict):
return make_response(400, {"message": "Invalid request"})
try:
mobile_number = validate_mobile_number(event.get("mobile_number"))
except ValueError:
return make_response(400, {"message": "Invalid mobile number"})
Why validate before the lookup?
The first check requires a dictionary-shaped event. The second check returns a controlled 400 response for an invalid mobile number.
Both checks finish before connection_factory() can run. Invalid input never reaches the database boundary.
- Replace the event lookup inside the existing find_customer_plan() call with the validated local value using this line:
customer = find_customer_plan(mobile_number, connection_factory)
Why use the local value?
The mobile_number variable contains the value that passed the strict gate. The lookup now receives only validated customer data.
- Save src/lambda_function.py.
Before you run the suite, do you expect the existing happy path to survive the new input gate?
- Check the existing behavior by running this command in the terminal from earlier:
python3 -m unittest discover
What should you see?
The three existing tests finish successfully. This proves the valid sample number still reaches the parameterized lookup.
Does the valid lookup fail?
- Check that MOBILE_PATTERN uses exactly nine digits after +61.
- Check that lambda_handler() passes mobile_number into find_customer_plan().
Ask for help with the validation path: Help me compare my mobile-number validation with the expected handler flow.
Control missing and failed lookups
Database failures can contain private customer data or internal driver details. Privacy-safe logging records a fixed failure message while the handler returns a generic response.
- At the top of src/lambda_function.py, find the updated import block:
import re
import pymysql
import pymysql.cursors
What is here already?
The current imports support validation plus database access. The next change adds the standard logging module.
- Replace the import block with this final version:
import logging
import re
import pymysql
import pymysql.cursors
What changed?
The logging module provides the fixed error log used by the handler. The validation plus database imports stay in place.
- Add the module logger directly above MOBILE_PATTERN using this line:
LOGGER = logging.getLogger(__name__)
Why use a named logger?
The logger uses the module name src.lambda_function. The tests can capture that specific log stream without changing the handler response.
- In lambda_handler(), replace the direct customer lookup with this guarded lookup:
try:
customer = find_customer_plan(mobile_number, connection_factory)
except Exception:
LOGGER.error("Customer plan lookup failed")
return make_response(500, {"message": "Lookup failed"})
How are failures controlled?
- The try block covers connection creation plus query execution.
- The fixed log message avoids including the mobile number or raw exception text.
- The 500 response exposes only Lookup failed to the caller.
- The existing 404 branch handles a successful lookup with no matching row.
- Save src/lambda_function.py.
- Check that the existing paths still pass by running this command:
python3 -m unittest discover
What should you see?
The existing tests still finish successfully. The new failure branches are ready for focused test coverage.
Seeing a handler failure?
- Check that the guarded lookup remains inside lambda_handler().
- Check that LOGGER is defined before MOBILE_PATTERN.
- Check that the existing missing-customer branch remains below the new exception handler.
Get help with the handler structure: Help me find the structural difference in my controlled lookup flow.
That is a strong checkpoint. The handler now contains controlled paths for valid input, invalid input, missing customers, and database failures.
✔️ Awesome, I've got everything!
Great. Double check that you saved src/lambda_function.py before moving to the tests.
ⓧ I'd like to double check the full code
Compare your complete src/lambda_function.py file with this reference:
import logging
import re
import pymysql
import pymysql.cursors
LOGGER = logging.getLogger(__name__)
MOBILE_PATTERN = re.compile(r"\+61[0-9]{9}\Z")
LOOKUP_SQL = (
"SELECT mobile_number, plan "
"FROM customers "
"WHERE mobile_number = %s"
)
DATABASE_CONFIG = None
def build_connection(config):
return pymysql.connect(
host=config["host"],
user=config["username"],
password=config["password"],
database=config["database"],
port=config["port"],
connect_timeout=config["connect_timeout"],
read_timeout=config["read_timeout"],
write_timeout=config["write_timeout"],
cursorclass=pymysql.cursors.DictCursor,
)
def connection_factory():
if DATABASE_CONFIG is None:
raise RuntimeError("Database configuration is not available")
return build_connection(DATABASE_CONFIG)
def validate_mobile_number(value):
if not isinstance(value, str) or MOBILE_PATTERN.fullmatch(value) is None:
raise ValueError("mobile_number must be +61 followed by 9 digits")
return value
def find_customer_plan(mobile_number, factory):
connection = factory()
try:
with connection.cursor() as cursor:
cursor.execute(LOOKUP_SQL, (mobile_number,))
row = cursor.fetchone()
finally:
connection.close()
if row is None:
return None
return {
"mobile_number": row["mobile_number"],
"plan": row["plan"],
}
def make_response(status_code, body):
return {
"statusCode": status_code,
"body": body,
}
def lambda_handler(event, context):
del context
if not isinstance(event, dict):
return make_response(400, {"message": "Invalid request"})
try:
mobile_number = validate_mobile_number(event.get("mobile_number"))
except ValueError:
return make_response(400, {"message": "Invalid mobile number"})
try:
customer = find_customer_plan(mobile_number, connection_factory)
except Exception:
LOGGER.error("Customer plan lookup failed")
return make_response(500, {"message": "Lookup failed"})
if customer is None:
return make_response(404, {"message": "Customer not found"})
return make_response(200, customer)
Prove every outcome
Each unit test locks one part of the public contract in place. These tests also prove that invalid input avoids the connection factory while database failures still close an opened connection.
- In tests/test_lambda_function.py, add this test at the end of CustomerPlanLookupTests to cover injection-shaped input:
def test_rejects_injection_shaped_mobile_before_database_access(self):
invalid_mobile = "+61400000000' OR '1'='1"
with patch("src.lambda_function.connection_factory") as factory:
response = lambda_handler({"mobile_number": invalid_mobile}, None)
self.assertEqual(response["statusCode"], 400)
self.assertEqual(response["body"], {"message": "Invalid mobile number"})
factory.assert_not_called()
What does this test prove?
The test sends a value containing extra SQL-shaped text. It expects the handler to return 400 before the database factory runs.
- Save tests/test_lambda_function.py.
- Run the expanded suite with this command:
python3 -m unittest discover
What should you see?
Four tests finish successfully. The new test proves malformed input cannot cross the database boundary.
Does the factory get called?
- Check that input validation appears above the call to find_customer_plan().
- Check that the invalid value contains the extra quote characters shown in the test.
Get help tracing the call order: Help me find why invalid input still reaches my connection factory.
- Add this missing-customer test below the injection-shaped input test:
def test_returns_not_found_for_missing_customer(self):
cursor = FakeCursor(row=None)
connection = FakeConnection(cursor)
with patch(
"src.lambda_function.connection_factory",
return_value=connection,
):
response = lambda_handler({"mobile_number": MOBILE_NUMBER}, None)
self.assertEqual(response["statusCode"], 404)
self.assertEqual(response["body"], {"message": "Customer not found"})
self.assertTrue(connection.closed)
What does this test prove?
A cursor result of None represents a valid query with no matching customer. The handler returns 404 while the connection still closes.
- Save tests/test_lambda_function.py.
- Run the suite again with this command:
python3 -m unittest discover
What should you see?
Five tests finish successfully. The suite now covers both found and missing customers.
Does the not-found test fail?
- Check that FakeCursor receives row=None.
- Check that the 404 branch remains below the database exception handler.
Ask for help with the missing-row path: Help me trace why my missing-customer test does not return 404.
- Replace the existing FakeCursor class with this error-aware version:
class FakeCursor:
def __init__(self, row=None, error=None):
self.row = row
self.error = error
self.executions = []
def execute(self, query, args=None):
self.executions.append((query, args))
if self.error is not None:
raise self.error
return 1
def fetchone(self):
return self.row
def __enter__(self):
return self
def __exit__(self, exc_type, exc_value, traceback):
return False
How does the fake cursor change?
The optional error value lets a test trigger a failure during execute(). The cursor still records every query before raising that failure.
- Save tests/test_lambda_function.py.
- Confirm the existing tests still pass by running this command:
python3 -m unittest discover
What should you see?
The five existing tests still finish successfully. The fake cursor can now simulate a database exception without changing its successful behavior.
Did the fake cursor break a passing test?
- Check that error defaults to None.
- Check that return 1 remains after the conditional raise.
Get help comparing the fake: Help me find why my updated FakeCursor broke the earlier tests.
- Add this controlled-failure test at the end of CustomerPlanLookupTests.
def test_returns_controlled_error_and_closes_connection(self):
cursor = FakeCursor(error=RuntimeError("database unavailable"))
connection = FakeConnection(cursor)
with patch(
"src.lambda_function.connection_factory",
return_value=connection,
), self.assertLogs("src.lambda_function", level="ERROR"):
response = lambda_handler({"mobile_number": MOBILE_NUMBER}, None)
self.assertEqual(response["statusCode"], 500)
self.assertEqual(response["body"], {"message": "Lookup failed"})
self.assertNotIn("database unavailable", str(response))
self.assertTrue(connection.closed)
What does the failure test prove?
- The fake cursor raises a database exception during query execution.
- The test captures the module's ERROR log without exposing it in the response.
- The response contains a controlled 500 status plus a generic message.
- The final assertion proves the finally block closes the connection after the exception.
- Save tests/test_lambda_function.py.
✔️ Awesome, I've got everything!
Great. Double check that you saved tests/test_lambda_function.py before running the final checks.
ⓧ I'd like to double check the full code
Compare your complete tests/test_lambda_function.py file with this reference:
from unittest import TestCase
from unittest.mock import patch
from src.lambda_function import LOOKUP_SQL, lambda_handler
MOBILE_NUMBER = "+61400000000"
CUSTOMER_ROW = {
"mobile_number": MOBILE_NUMBER,
"plan": "Everyday 20GB",
}
class FakeCursor:
def __init__(self, row=None, error=None):
self.row = row
self.error = error
self.executions = []
def execute(self, query, args=None):
self.executions.append((query, args))
if self.error is not None:
raise self.error
return 1
def fetchone(self):
return self.row
def __enter__(self):
return self
def __exit__(self, exc_type, exc_value, traceback):
return False
class FakeConnection:
def __init__(self, cursor):
self.fake_cursor = cursor
self.closed = False
def cursor(self):
return self.fake_cursor
def close(self):
self.closed = True
class CustomerPlanLookupTests(TestCase):
def test_returns_customer_plan(self):
cursor = FakeCursor(row=CUSTOMER_ROW)
connection = FakeConnection(cursor)
with patch(
"src.lambda_function.connection_factory",
return_value=connection,
):
response = lambda_handler({"mobile_number": MOBILE_NUMBER}, None)
self.assertEqual(response["statusCode"], 200)
self.assertEqual(response["body"], CUSTOMER_ROW)
self.assertTrue(connection.closed)
def test_keeps_sql_and_mobile_number_separate(self):
cursor = FakeCursor(row=CUSTOMER_ROW)
connection = FakeConnection(cursor)
with patch(
"src.lambda_function.connection_factory",
return_value=connection,
):
lambda_handler({"mobile_number": MOBILE_NUMBER}, None)
self.assertEqual(
cursor.executions,
[(LOOKUP_SQL, (MOBILE_NUMBER,))],
)
def test_rejects_injection_shaped_mobile_before_database_access(self):
invalid_mobile = "+61400000000' OR '1'='1"
with patch("src.lambda_function.connection_factory") as factory:
response = lambda_handler({"mobile_number": invalid_mobile}, None)
self.assertEqual(response["statusCode"], 400)
self.assertEqual(response["body"], {"message": "Invalid mobile number"})
factory.assert_not_called()
def test_returns_not_found_for_missing_customer(self):
cursor = FakeCursor(row=None)
connection = FakeConnection(cursor)
with patch(
"src.lambda_function.connection_factory",
return_value=connection,
):
response = lambda_handler({"mobile_number": MOBILE_NUMBER}, None)
self.assertEqual(response["statusCode"], 404)
self.assertEqual(response["body"], {"message": "Customer not found"})
self.assertTrue(connection.closed)
def test_returns_controlled_error_and_closes_connection(self):
cursor = FakeCursor(error=RuntimeError("database unavailable"))
connection = FakeConnection(cursor)
with patch(
"src.lambda_function.connection_factory",
return_value=connection,
), self.assertLogs("src.lambda_function", level="ERROR"):
response = lambda_handler({"mobile_number": MOBILE_NUMBER}, None)
self.assertEqual(response["statusCode"], 500)
self.assertEqual(response["body"], {"message": "Lookup failed"})
self.assertNotIn("database unavailable", str(response))
self.assertTrue(connection.closed)
Before you run the final checks, do you expect every contract path to pass without revealing the simulated database detail?
- Run the complete test suite with this command:
python3 -m unittest discover
What should you see?
All six tests finish successfully. The suite now covers successful lookup, parameter separation, invalid input, missing customers, database failure, and smoke-test discovery.
- Check every Python file for compilation errors by running this command:
python3 -m compileall .
What should you see?
Python scans the project files and completes without syntax errors. Your source module plus test suite are valid Python.
Does a final check fail?
- Use the failing test name to identify the response path that differs from the reference.
- Use the file path from the compile output to locate any indentation or bracket mismatch.
- Compare both files with the full-code tabs above.
Ask for help with the final failure: Help me diagnose my failing test or Python compilation error.
You have locked down the customer lookup contract. Invalid input stops before database access while every database path returns a controlled response.
Secret mission
Add a Privacy Regression Guard
A fixed response protects callers from database details. This privacy regression guard also protects the error log from future debugging changes.
Clean Up Your Resources
Clean Up Your Resources
This project runs entirely inside the local customer-plan-lookup-core folder with no ongoing costs. The options below cover keeping, pausing, or deleting your local work.
Resources you used:
- Your local customer-plan-lookup-core folder containing the source code and automated tests.
Keep everything running
No action is needed. Choose this option if you want to extend the secure lookup core in a later project.
- Keep the customer-plan-lookup-core folder available for future database integration work.
- Keep PyMySQL 1.2.0 installed for future Python projects.
- Run the existing tests whenever you change the handler contract.
Pause - I'll come back to this later
There is no running process to shut down. Your test suite exits automatically after each run.
- Leave the customer-plan-lookup-core folder on your Mac.
- Return to the folder later in Visual Studio Code when you are ready to continue.
Delete - I don't want to use this again
This choice permanently removes your code and tests. The command below targets only the customer-plan-lookup-core folder.
- Save any project files you want to keep in another location.
- Open an integrated terminal in the Visual Studio Code window from earlier.
- Remove the customer-plan-lookup-core folder by running these commands:
cd ..
rm -rf customer-plan-lookup-core
What do these commands do?
The first line moves the terminal into the folder that contains your project. The second line permanently removes the named project folder.
This deletes requirements.txt plus the src/ and tests/ folders.
- Check the file tree in Visual Studio Code. You should no longer see requirements.txt or the src/ and tests/ folders.
Still see the project files?
- Check that the terminal opened inside customer-plan-lookup-core before you ran the commands.
- Ask for help diagnosing the deletion problem.
Nice Work!
Nice Work!
You did it! You built a local customer plan lookup that validates each request before using parameterized SQL. Its passing tests prove the handler returns controlled outcomes without exposing private failure details.
You've learned how to:
- Build an AWS Lambda-shaped handler that accepts an event before returning JSON-compatible success or error responses.
- Reject malformed or injection-shaped mobile numbers through strict input validation before the connection factory runs.
- Keep LOOKUP_SQL stable with PyMySQL parameter binding. Use unit testing to prove each connection closes after successful or failed lookups.
- Secret Mission: Added a privacy regression test that fails if logs expose a mobile number or raw database error details.
Ready to quiz yourself?