add tests to email

This commit is contained in:
2025-11-26 23:33:12 -03:00
parent 5859248781
commit ab7e4ea38b
9 changed files with 1040 additions and 718 deletions

View File

@@ -103,6 +103,7 @@ def unlink(org_id: str, user_id: str):
def _create_user(user: User, org: Org) -> bool: def _create_user(user: User, org: Org) -> bool:
now_ = now() now_ = now()
user_id = uuid4() user_id = uuid4()
email_verified = '@users.noreply.saladeaula.digital' in user.email
try: try:
with dyn.transact_writer() as transact: with dyn.transact_writer() as transact:
@@ -111,7 +112,7 @@ def _create_user(user: User, org: Org) -> bool:
**user.model_dump(), **user.model_dump(),
'id': user_id, 'id': user_id,
'sk': '0', 'sk': '0',
'email_verified': False, 'email_verified': email_verified,
'tenant_id': {org.id}, 'tenant_id': {org.id},
# Post-migration: uncomment the folloing line # Post-migration: uncomment the folloing line
# 'org_id': {org.id}, # 'org_id': {org.id},
@@ -123,7 +124,7 @@ def _create_user(user: User, org: Org) -> bool:
'id': user_id, 'id': user_id,
# Post-migration: rename `emails` to `EMAIL` # Post-migration: rename `emails` to `EMAIL`
'sk': f'emails#{user.email}', 'sk': f'emails#{user.email}',
'email_verified': False, 'email_verified': email_verified,
'email_primary': True, 'email_primary': True,
'created_at': now_, 'created_at': now_,
} }

View File

@@ -68,8 +68,8 @@ def add(
) )
transact.put( transact.put(
item={ item={
'id': 'EMAIL_VERIFICATION', 'id': user_id,
'sk': uuid4(), 'sk': f'EMAIL_VERIFICATION#{uuid4()}',
'name': name, 'name': name,
'email': email, 'email': email,
'user_id': user_id, 'user_id': user_id,
@@ -81,6 +81,27 @@ def add(
return JSONResponse(status_code=HTTPStatus.CREATED) return JSONResponse(status_code=HTTPStatus.CREATED)
@router.post('/<user_id>/emails/<email>/request-verification')
def request_verification(user_id: str, email: Annotated[EmailStr, Path]):
now_ = now()
name = dyn.collection.get_item(
KeyPair(user_id, SortKey('0', path_spec='name')),
raise_on_error=False,
)
dyn.put_item(
item={
'id': user_id,
'sk': f'EMAIL_VERIFICATION#{uuid4()}',
'name': name,
'email': email,
'user_id': user_id,
'ttl': ttl(start_dt=now_, days=30),
'created_at': now_,
}
)
return JSONResponse(status_code=HTTPStatus.NO_CONTENT)
class EmailVerificationNotFoundError(NotFoundError): ... class EmailVerificationNotFoundError(NotFoundError): ...
@@ -88,15 +109,18 @@ class EmailVerificationNotFoundError(NotFoundError): ...
def verify(user_id: str, hash: str): def verify(user_id: str, hash: str):
email = dyn.collection.get_item( email = dyn.collection.get_item(
KeyPair( KeyPair(
pk='EMAIL_VERIFICATION', pk=user_id,
sk=SortKey(hash, path_spec='email'), sk=SortKey(f'EMAIL_VERIFICATION#{hash}', path_spec='email'),
), ),
exc_cls=EmailVerificationNotFoundError, exc_cls=EmailVerificationNotFoundError,
) )
with dyn.transact_writer() as transact: with dyn.transact_writer() as transact:
transact.delete(key=KeyPair('EMAIL_VERIFICATION', hash)) transact.delete(
key=KeyPair(user_id, f'EMAIL_VERIFICATION#{hash}'),
)
transact.update( transact.update(
# Post-migration (users): rename `emails` to `EMAIL`
key=KeyPair(user_id, f'emails#{email}'), key=KeyPair(user_id, f'emails#{email}'),
update_expr='SET email_verified = :true, updated_at = :now', update_expr='SET email_verified = :true, updated_at = :now',
expr_attr_values={ expr_attr_values={

View File

@@ -1,7 +1,9 @@
import json import json
from http import HTTPMethod, HTTPStatus from http import HTTPMethod, HTTPStatus
from ..conftest import HttpApiProxy, LambdaContext from layercake.dynamodb import DynamoDBPersistenceLayer, KeyPair
from ...conftest import HttpApiProxy, LambdaContext
def test_get_emails( def test_get_emails(
@@ -23,6 +25,35 @@ def test_get_emails(
assert len(body['items']) == 2 assert len(body['items']) == 2
def test_add_email(
app,
seeds,
http_api_proxy: HttpApiProxy,
lambda_context: LambdaContext,
dynamodb_persistence_layer: DynamoDBPersistenceLayer,
):
r = app.lambda_handler(
http_api_proxy(
raw_path='/users/15bacf02-1535-4bee-9022-19d106fd7518/emails',
method=HTTPMethod.POST,
body={
'email': 'osergiosiqueira+pytest@gmail.com',
},
),
lambda_context,
)
assert r['statusCode'] == HTTPStatus.CREATED
email_verification = dynamodb_persistence_layer.collection.query(
KeyPair(
'15bacf02-1535-4bee-9022-19d106fd7518',
'EMAIL_VERIFICATION',
)
)
assert 'name' in email_verification['items'][0]
assert email_verification['items'][0]['email'] == 'osergiosiqueira+pytest@gmail.com'
def test_email_as_primary( def test_email_as_primary(
app, app,
seeds, seeds,
@@ -45,7 +76,7 @@ def test_email_as_primary(
assert r['statusCode'] == HTTPStatus.NO_CONTENT assert r['statusCode'] == HTTPStatus.NO_CONTENT
def test_get_orgs( def test_remove_emal(
app, app,
seeds, seeds,
http_api_proxy: HttpApiProxy, http_api_proxy: HttpApiProxy,
@@ -53,10 +84,10 @@ def test_get_orgs(
): ):
r = app.lambda_handler( r = app.lambda_handler(
http_api_proxy( http_api_proxy(
raw_path='/users/213a6682-2c59-4404-9189-12eec0a846d4/orgs', raw_path='/users/15bacf02-1535-4bee-9022-19d106fd7518/emails/osergiosiqueira@gmail.com',
method=HTTPMethod.GET, method=HTTPMethod.DELETE,
), ),
lambda_context, lambda_context,
) )
assert r['statusCode'] == HTTPStatus.OK assert r['statusCode'] == HTTPStatus.NO_CONTENT

View File

@@ -0,0 +1,20 @@
from http import HTTPMethod, HTTPStatus
from ...conftest import HttpApiProxy, LambdaContext
def test_get_orgs(
app,
seeds,
http_api_proxy: HttpApiProxy,
lambda_context: LambdaContext,
):
r = app.lambda_handler(
http_api_proxy(
raw_path='/users/213a6682-2c59-4404-9189-12eec0a846d4/orgs',
method=HTTPMethod.GET,
),
lambda_context,
)
assert r['statusCode'] == HTTPStatus.OK

View File

@@ -93,15 +93,15 @@ wheels = [
[[package]] [[package]]
name = "aws-lambda-powertools" name = "aws-lambda-powertools"
version = "3.20.0" version = "3.23.0"
source = { registry = "https://pypi.org/simple" } source = { registry = "https://pypi.org/simple" }
dependencies = [ dependencies = [
{ name = "jmespath" }, { name = "jmespath" },
{ name = "typing-extensions" }, { name = "typing-extensions" },
] ]
sdist = { url = "https://files.pythonhosted.org/packages/83/c3/ee5b3ed77be236ff0accafabb137a3a1431925ae11b50fc31fab09d60db1/aws_lambda_powertools-3.20.0.tar.gz", hash = "sha256:97cdaa52060972a59d0e769e197d6bffe5a1869c54b6a0c6c69bf31656de5bdd", size = 697927, upload-time = "2025-09-09T09:56:11.78Z" } sdist = { url = "https://files.pythonhosted.org/packages/38/24/78f320a310d98df8c831e15c5f04fec20ba4958253deb165ab2d10d3392b/aws_lambda_powertools-3.23.0.tar.gz", hash = "sha256:30ab45960989dd75a4d84de4f156509458f8782038d532eee2f815488d7cc929", size = 702835, upload-time = "2025-11-13T16:44:23.659Z" }
wheels = [ wheels = [
{ url = "https://files.pythonhosted.org/packages/3b/37/d036d4c33f0d943dbd5257d2c26f5a60375452705526b34977357f8faebf/aws_lambda_powertools-3.20.0-py3-none-any.whl", hash = "sha256:1641eaf38e59470a28262e34d5d447bf6713223ea8896bf5ce442ffb3418c9c9", size = 842168, upload-time = "2025-09-09T09:56:09.847Z" }, { url = "https://files.pythonhosted.org/packages/70/48/f59597b0acbe3bcd829ae5b13b49a29039c5b2a5a6771f765ad3f3f576a3/aws_lambda_powertools-3.23.0-py3-none-any.whl", hash = "sha256:f3d16f1b0304c686cc956ecf0f6f8907d21992a4a5070e2388c21571d8c84cc2", size = 848256, upload-time = "2025-11-13T16:44:21.459Z" },
] ]
[package.optional-dependencies] [package.optional-dependencies]
@@ -622,7 +622,7 @@ dependencies = [
requires-dist = [ requires-dist = [
{ name = "arnparse", specifier = ">=0.0.2" }, { name = "arnparse", specifier = ">=0.0.2" },
{ name = "authlib", specifier = ">=1.6.5" }, { name = "authlib", specifier = ">=1.6.5" },
{ name = "aws-lambda-powertools", extras = ["all"], specifier = ">=3.18.0" }, { name = "aws-lambda-powertools", extras = ["all"], specifier = ">=3.23.0" },
{ name = "dictdiffer", specifier = ">=0.9.0" }, { name = "dictdiffer", specifier = ">=0.9.0" },
{ name = "ftfy", specifier = ">=6.3.1" }, { name = "ftfy", specifier = ">=6.3.1" },
{ name = "glom", specifier = ">=24.11.0" }, { name = "glom", specifier = ">=24.11.0" },

View File

@@ -1,6 +1,7 @@
import { LockIcon } from 'lucide-react'
import type { Route } from './+types' import type { Route } from './+types'
import { LockIcon } from 'lucide-react'
export function meta({}: Route.MetaArgs) { export function meta({}: Route.MetaArgs) {
return [{ title: 'Acesso negado · EDUSEG®' }] return [{ title: 'Acesso negado · EDUSEG®' }]
} }

View File

@@ -8,7 +8,7 @@ authors = [
] ]
requires-python = ">=3.12" requires-python = ">=3.12"
dependencies = [ dependencies = [
"aws-lambda-powertools[all]>=3.18.0", "aws-lambda-powertools[all]>=3.23.0",
"ftfy>=6.3.1", "ftfy>=6.3.1",
"glom>=24.11.0", "glom>=24.11.0",
"orjson>=3.10.15", "orjson>=3.10.15",

1645
layercake/uv.lock generated

File diff suppressed because it is too large Load Diff