# PRD — unityAWS

Evidence date: 2026-09-30.

Source: `git@bitbucket.org:overdrive/unityaws.git`, `master` at `e9f10da` (2019-10-08).

There is no prior PRD in the source repository or in Silicon Ark. Requirements below are recovered from `lambda_function.py`, `login.py`, `config.py`, `lambda/lambda_function.py`, `bitbucket-pipelines.yml`, `lambda.zip`, and the ten commits on that revision. Provenance is labelled. The database password is omitted.

## Product purpose

**Inferred.** A Python AWS Lambda, named `qaFunction` by the pipeline, intended to accept a login action against MySQL database `unityqa`.

**Implemented.** The handler recognises action `login` and calls `login()`. That function does not read the database and always returns the same failure object. Any other action that survives JSON parsing returns HTTP 200 and a JSON empty string.

**Documented** only by the README heading `# unityAWS` and by commit subjects ("New login file", "Json decode and loaded to body", "zero out response").

**Unknown / Requires Confirmation:** the product name shown to a person, the caller, and whether this function was abandoned in favour of `unityNodejsApi` (Lambda `unityAuth`, same RDS host and database name, commits starting 2019-11-11). This repository does not name that other repository.

## Users and interactions

| Actor | Evidence | Status |
|---|---|---|
| Caller sending a Lambda event | `lambda_handler(event, context)` in the root file | **Implemented**. The caller is not named |
| Person identified by `username` and `password` | Read from the event only when action is `login` | **Implemented** as event keys. They are not checked |
| Operator of Bitbucket Pipelines | `bitbucket-pipelines.yml` default pipeline | **Implemented** as YAML. Who may run it is **Unknown** |
| Database role `dbadmin` | `config.py` | **Implemented** as a connection user. No grant or role table is in git |

No screen, session, cookie, role, or user-admin function exists in this repository.

## Systems the product talks to

1. AWS Lambda function `qaFunction` in region `us-east-2`. The pipeline updates its zip. The function resource itself is not in git.
2. MySQL on host `unityqa.csigt2kcstpl.us-east-2.rds.amazonaws.com`, database `unityqa`, user `dbadmin`, connect timeout 5 seconds. Password is in `config.py` and is omitted here. Driver is vendored PyMySQL 0.9.3.
3. Bitbucket Pipelines, image `python:3.5.7`, pipe `atlassian/aws-lambda-deploy:0.2.1`. AWS keys are pipeline variables, not files.

No HTTP client, queue, mailer, object store, or token signer is present.

## Major workflows

### WF-1 — Pipeline packages and updates Lambda

**Implemented** in `bitbucket-pipelines.yml`:

1. Default pipeline. No branch filter. No test step.
2. Image `python:3.5.7`.
3. Step "Build and package": `apt-get update && apt-get install -y zip`, then `zip -r code.zip *`. The artifact is `code.zip`.
4. Step "Update Lambda code": pipe command `update`, region `us-east-2`, `FUNCTION_NAME` `qaFunction`, `ZIP_FILE` `code.zip`.

History of the zip path: `437b8c6` used `zip -r code.zip -r /lambda/` (a second `-r` and an absolute `/lambda/`). `9a9819c` used `zip -r code.zip lambda/`. `661f10a` switched to `zip -r code.zip *` when the handler moved to the repository root.

Shell `*` does not expand dotfiles, so `.git` is not in a pipeline zip. `pymysql`, `__pycache__`, `config.py`, `login.py`, `lambda/`, and `lambda.zip` are not dotfiles, so a pipeline run at HEAD would include them.

Whether a pipeline has succeeded, and whether the Lambda handler property is `lambda_function.lambda_handler`, is **Unknown / Requires Confirmation**.

### WF-2 — Handler dispatch

**Implemented** in root `lambda_function.py` at `e9f10da`:

1. `from login import login` runs at import. That import connects to MySQL or exits. See WF-4.
2. `body = json.loads(event["body"])`. `event["body"]` must be a string of JSON, not an already-parsed object. `json` is the standard library.
3. `response` starts as `""`.
4. If `body["action"] == "login"`, `response` becomes `login(event['username'], event['password'])`.
5. The handler returns `statusCode` 200, header `Access-Control-Allow-Origin: *`, and `body` `json.dumps(response)`.
6. `context` is unused. `sys` is imported and unused.

There is no `else`. An action other than `login` leaves `response` as `""`. `json.dumps("")` is the two-character JSON text `""` (a JSON string), not `{}` and not the login object.

`username` and `password` are read from the event root. They are not read from the parsed body. That split was introduced at `9ddf51c`, when `action` moved into `event["body"]` and the credential keys did not.

### WF-3 — Login

**Implemented** in `login.login`. Provenance of the fixed return: present since `login.py` was added at `30c1d6c`. No later commit adds a query.

1. The function accepts `email` and `password`. The handler passes `event['username']` into `email`. Neither parameter is read.
2. The return value is always `{"success": "false", "reason": "invalid"}`. `success` is the string `"false"`, not the boolean `false`.
3. The module-level connection object `conn` is not used. No cursor is created. No SQL string exists in `login.py`.

The handler then JSON-encodes that dict, so a reached login call responds with HTTP 200 and body `{"success": "false", "reason": "invalid"}`.

### WF-4 — Database connection at import

**Implemented** at module import in `login.py` and, separately, in `lambda/lambda_function.py`:

1. Read `db_endpoint`, `db_username`, `db_password`, and `db_name` from `config.py`.
2. `port = 3306` is assigned and not passed to `connect`. PyMySQL's own default port is 3306. Using 3306 is **Inferred** from that unused variable plus the driver default.
3. `pymysql.connect(host, user=name, passwd=password, db=db_name, connect_timeout=5)`.
4. On any exception, log `ERROR: Unexpected error: Could not connect to MySql instance.` and `sys.exit()`.
5. On success, log `SUCCESS: Connection to RDS mysql instance succeeded`.

Because the root handler imports `login` at load, a failed connect aborts the interpreter before `lambda_handler` runs. The stub login response is then unreachable.

The `except` is bare. The original exception text is not logged.

### WF-5 — Second handler under `lambda/`

**Implemented** in `lambda/lambda_function.py` since `6ac019f`. The root handler does not import this file.

1. The same import-time connect as WF-4.
2. `lambda_handler` ignores `event` and `context`.
3. Return is `statusCode` 200 and `body` `json.dumps('Lambda , Unity')`. There is no CORS header.

The pipeline zip of `*` includes this file. It runs only if the Lambda handler setting points at `lambda.lambda_function.lambda_handler` or an equivalent. That setting is **Unknown / Requires Confirmation**.

### WF-6 — Committed zip, distinct from HEAD

**Implemented** artifact `lambda.zip`, added at `661f10a` and not changed afterward.

Its `lambda_function.py` matches the root handler as it was at the folder move, before CORS and before `login.py`: import-time MySQL connect, return body `Lambda , Unity`, no CORS header. The zip listing has `config.py`, `pymysql/`, `PyMySQL-0.9.3.dist-info/`, and `__pycache__/`. It does not contain `login.py`, `lambda/lambda_function.py`, `README.md`, or `bitbucket-pipelines.yml`.

The pipeline uploads `code.zip`, not `lambda.zip`. Nothing in the YAML builds or publishes `lambda.zip`.

## Functional requirements

### FR-001 — Accept a JSON body and read `action`

Requirement: The handler parses `event["body"]` as JSON and compares `action` to the string `login`.

Who: The Lambda runtime, calling the root handler.

Trigger: An invocation whose event has a string `body`.

Inputs: `event["body"]` JSON with an `action` field.

Expected behaviour: `login` calls `login()`. Any other `action` skips that call.

Outputs: See FR-002 and FR-003.

Validation: `json.loads`. No schema check.

Error conditions: Missing `body`, non-JSON `body`, or missing `action` raises before the return. The handler does not catch it. Lambda's platform error shape is **Unknown** because it is not in this repo.

Evidence: root `lambda_function.py`. Implementation: **Implemented**. Confidence: high.

### FR-002 — Login returns a fixed failure

Requirement: Action `login` responds with HTTP 200 and body `{"success": "false", "reason": "invalid"}`.

Who: The caller.

Trigger: `body["action"]` equals `login`, and `event["username"]` and `event["password"]` are present so the call does not raise.

Inputs: `username` and `password` on the event root. Values are ignored.

Expected behaviour: The dict above, JSON-encoded. `success` is a string.

Outputs: `statusCode` 200, CORS header `*`, that body.

Validation: None on the credential values.

Error conditions: Missing `username` or `password` raises `KeyError`. A failed import-time database connection never reaches this return.

Evidence: `login.py`, root `lambda_function.py`. Implementation: **Implemented**. Confidence: high.

### FR-003 — Any other action returns an empty JSON string

Requirement: If parsing succeeds and `action` is not `login`, the handler still returns HTTP 200.

Who: The caller.

Trigger: Parsed `action` other than `login`.

Inputs: Any other `action` string, including empty.

Expected behaviour: `response` stays `""`. Body is JSON `""`.

Outputs: `statusCode` 200 and the CORS header.

Validation: None.

Error conditions: None on this path once `action` exists.

Evidence: `e9f10da` initialises `response` so the `NameError` from `9ddf51c` does not occur. Implementation: **Implemented**. Confidence: high.

### FR-004 — CORS on the root handler

Requirement: A response the root handler returns includes `Access-Control-Allow-Origin: *`.

Who: A browser caller, if one exists. No browser is in this repo.

Trigger: The root handler returns.

Inputs: None beyond the invocation.

Expected behaviour: That single header. No `Access-Control-Allow-Headers`, no `Access-Control-Allow-Methods`, no OPTIONS branch.

Outputs: The header on the 200 response.

Error conditions: The `lambda/` handler does not set the header. Platform errors from a raised exception do not pass through this return.

Evidence: root `lambda_function.py` since `6ac019f`. Implementation: **Implemented**. Confidence: high.

### FR-005 — Connect to RDS before handling the event

Requirement: Importing `login` opens a PyMySQL connection to the host, user, password, and database in `config.py`, with `connect_timeout` 5.

Who: The Lambda runtime, at cold start of a deployment that imports `login`.

Trigger: Import.

Inputs: `config.py` values. Password omitted from this PRD.

Expected behaviour: Success logs `SUCCESS: Connection to RDS mysql instance succeeded`. Failure logs the unexpected-error line and exits.

Outputs: A module-global connection that no function queries.

Validation: The driver and the server. No application retry.

Error conditions: Any exception during connect. The original error is discarded.

Evidence: `login.py`, `config.py`. Implementation: **Implemented**. Confidence: high.

### FR-006 — Pipeline updates `qaFunction`

Requirement: The default Bitbucket pipeline zips the tree to `code.zip` and updates Lambda `qaFunction` in `us-east-2`.

Who: Bitbucket Pipelines, using the AWS keys configured on the repository. Those keys are not in git.

Trigger: A push that runs the default pipeline. Branch filters are absent, so which branches do that is the Bitbucket default. Only `master` exists in git.

Inputs: The working tree and pipeline variables `AWS_ACCESS_KEY_ID` and `AWS_SECRET_ACCESS_KEY`.

Expected behaviour: Pipe `atlassian/aws-lambda-deploy:0.2.1`, command `update`, zip `code.zip`.

Outputs: An updated function package, if AWS accepts the call. No run log is in git.

Validation: None in the YAML. No test step.

Error conditions: The pipe's failure behaviour is not described in this repo. **Unknown / Requires Confirmation.**

Evidence: `bitbucket-pipelines.yml`. Implementation: **Implemented** as YAML. Confidence: high for the file, low for live deploy state.

## Important constraints

- Credential values live in `config.py` in git. This PRD does not copy them. Rotation and removal are a human decision.
- The handler does not authenticate the caller. No IAM authorizer, API key, or token check is in the source.
- HTTP status is 200 whenever the root handler returns, including the login failure object.
- Python versions disagree across artifacts: pipeline image 3.5.7, bytecode tag `cpython-37`, PyMySQL 0.9.3 classifiers through 3.7. The Lambda runtime setting is not in git.
- `lambda.zip` is not the file the pipeline deploys, and it does not match HEAD.

## Edge cases

| Situation | Behaviour in the root handler | Provenance |
|---|---|---|
| `event` has no `body` | `KeyError` / `TypeError` from the subscript, uncaught | **Implemented** |
| `body` is not JSON text | `json.JSONDecodeError`, uncaught | **Implemented** |
| JSON has no `action` | `KeyError`, uncaught | **Implemented** |
| `action` is `login` and `username` or `password` is absent | `KeyError`, uncaught | **Implemented** |
| `action` is `login` and the password is wrong, empty, or correct | Same failure object. Password is not compared | **Implemented** |
| `action` is anything else | HTTP 200, body JSON `""` | **Implemented** since `e9f10da` |
| RDS connect fails at import | Process exits. Handler does not run | **Implemented** |
| RDS connect succeeds | Connection kept and unused. Login still returns invalid | **Implemented** |
| Deployment is the committed `lambda.zip` rather than HEAD | Body `Lambda , Unity`, no login, no CORS header, still connects at import | **Implemented** in the zip |
| Handler property points at `lambda/lambda_function.py` | Same stub as the zip's handler, plus a live tree that also contains the root handler | **Implemented** file. Which property is set is **Unknown** |
