From fe74aad360c15b55d9e22ff1339a9b07b98f1707 Mon Sep 17 00:00:00 2001 From: Jason Park <93040528+JasonNotJson@users.noreply.github.com> Date: Sun, 24 Sep 2023 19:14:03 +0900 Subject: [PATCH] feat: creating test lambda for forum get single thread (#330) --- lib/constructs/business/rest-api-service.ts | 23 +++++- lib/constructs/common/lambda-functions.ts | 19 ++++- src/lambda/test-get-single-thread/index.py | 82 +++++++++++++++++++++ src/lambda/test-get-single-thread/utils.py | 82 +++++++++++++++++++++ src/lambda/test-post-thread/index.py | 5 +- 5 files changed, 207 insertions(+), 4 deletions(-) create mode 100644 src/lambda/test-get-single-thread/index.py create mode 100644 src/lambda/test-get-single-thread/utils.py diff --git a/lib/constructs/business/rest-api-service.ts b/lib/constructs/business/rest-api-service.ts index dc01a19e7..5269d4379 100644 --- a/lib/constructs/business/rest-api-service.ts +++ b/lib/constructs/business/rest-api-service.ts @@ -837,6 +837,10 @@ export class ForumThreadsApiService extends RestApiService { forumThreadsFunctions.testPostFunction, { proxy: true }, ); + const testGetIntegration = new apigw.LambdaIntegration( + forumThreadsFunctions.testGetFunction, + { proxy: true }, + ); const getAllForumThreads = root.addMethod( apigw2.HttpMethod.GET, @@ -940,7 +944,23 @@ export class ForumThreadsApiService extends RestApiService { apigw2.HttpMethod.POST, testPostIntegration, { - operationName: 'testThread', + operationName: 'testPostThread', + methodResponses: [ + { + statusCode: '200', + responseParameters: lambdaRespParams, + }, + ], + authorizer: props.authorizer, + requestValidator: props.validator, + }, + ); + + const testGetForumThreads = testResource.addMethod( + apigw2.HttpMethod.GET, + testGetIntegration, + { + operationName: 'testGetThread', methodResponses: [ { statusCode: '200', @@ -973,6 +993,7 @@ export class ForumThreadsApiService extends RestApiService { }, '/forum/test': { [apigw2.HttpMethod.POST]: testPostForumThreads, + [apigw2.HttpMethod.GET]: testGetForumThreads, [apigw2.HttpMethod.OPTIONS]: optionsTestThreads, }, }; diff --git a/lib/constructs/common/lambda-functions.ts b/lib/constructs/common/lambda-functions.ts index 4a27ef7b5..3aefd6998 100644 --- a/lib/constructs/common/lambda-functions.ts +++ b/lib/constructs/common/lambda-functions.ts @@ -492,6 +492,7 @@ export class ForumThreadFunctions extends Construct { readonly patchFunction: lambda.Function; readonly deleteFunction: lambda.Function; readonly testPostFunction: lambda.Function; + readonly testGetFunction: lambda.Function; constructor(scope: Construct, id: string, props: FunctionsProps) { super(scope, id); @@ -650,7 +651,23 @@ export class ForumThreadFunctions extends Construct { { entry: 'src/lambda/test-post-thread', description: 'lambda to test forum functionalities', - functionName: 'test-forum-thread', + functionName: 'test-post-forum-thread', + logRetention: logs.RetentionDays.ONE_MONTH, + memorySize: 128, + role: DBPutRole, + runtime: lambda.Runtime.PYTHON_3_9, + timeout: Duration.seconds(3), + environment: props.envVars, + }, + ); + + this.testGetFunction = new lambda_py.PythonFunction( + this, + 'test-get-thread', + { + entry: 'src/lambda/test-get-single-thread', + description: 'lambda to test forum get functionalities', + functionName: 'test-get-forum-thread', logRetention: logs.RetentionDays.ONE_MONTH, memorySize: 128, role: DBPutRole, diff --git a/src/lambda/test-get-single-thread/index.py b/src/lambda/test-get-single-thread/index.py new file mode 100644 index 000000000..ee49dba20 --- /dev/null +++ b/src/lambda/test-get-single-thread/index.py @@ -0,0 +1,82 @@ +from boto3.dynamodb.conditions import Key, Attr +from datetime import datetime +from utils import JsonPayloadBuilder, table, resp_handler, s3_client, bucket, generate_url + + +@resp_handler +def get_single_thread(board_id, thread_id, uid=""): + results = table.query( + KeyConditionExpression=Key("board_id").eq( + board_id) & Key("thread_id").eq(thread_id) + )["Items"] + + if not results: + raise LookupError + + item = results[0] + + if item["uid"] == uid: + table.update_item( + Key={ + "board_id": board_id, + "thread_id": thread_id, + }, + UpdateExpression="SET #v = #v + :incr, #nc = :newComment", + ConditionExpression="#uid = :uidValue", + ExpressionAttributeNames={ + '#v': 'views', + '#nc': 'new_comment', + '#uid': 'uid' + }, + ExpressionAttributeValues={ + ":incr": 1, + ":newComment": False, + ":uidValue": uid + } + ) + else: + # Increment the view count but do not update new_comment + table.update_item( + Key={ + "board_id": board_id, + "thread_id": thread_id, + }, + UpdateExpression="SET #v = #v + :incr", + ExpressionAttributeNames={ + '#v': 'views' + }, + ExpressionAttributeValues={ + ":incr": 1 + } + ) + + item["mod"] = False + if item["uid"] == uid: + item["mod"] = True + item['user_liked'] = uid in item.get('likes', []) + item['total_likes'] = len(item.get('likes', [])) + + if "object_key" in item: + bucket_name = bucket + presigned_url = generate_url(bucket_name, item["object_key"]) + if presigned_url: + item["url"] = presigned_url + + item.pop('uid', None) + item.pop('likes', None) + item.pop('object_key', None) + + body = JsonPayloadBuilder().add_status( + True).add_data(item).add_message('').compile() + return body + + +def handler(event, context): + params = { + "board_id": event["queryStringParameters"]["board_id"], + "thread_id": event["queryStringParameters"]["thread_id"], + } + if "uid" in event["queryStringParameters"]: + params["uid"] = event["queryStringParameters"]["uid"] + + return get_single_thread(**params) diff --git a/src/lambda/test-get-single-thread/utils.py b/src/lambda/test-get-single-thread/utils.py new file mode 100644 index 000000000..28cd921a5 --- /dev/null +++ b/src/lambda/test-get-single-thread/utils.py @@ -0,0 +1,82 @@ +import boto3 +import json +import logging +import os +from decimal import Decimal + +db = boto3.resource("dynamodb", region_name="ap-northeast-1") +table = db.Table(os.getenv('TABLE_NAME')) + +s3_client = boto3.client('s3') +bucket = os.getenv('BUCKET_NAME') + + +class DecimalEncoder(json.JSONEncoder): + def default(self, obj): + if isinstance(obj, Decimal): + return float(obj) + return json.JSONEncoder.default(self, obj) + + +class JsonPayloadBuilder: + payload = {} + + def add_status(self, success): + self.payload['success'] = success + return self + + def add_data(self, data): + self.payload['data'] = data + return self + + def add_message(self, msg): + self.payload['message'] = msg + return self + + def compile(self): + return json.dumps(self.payload, cls=DecimalEncoder, ensure_ascii=False).encode('utf8') + + +def api_response(code, body): + return { + "isBase64Encoded": False, + "statusCode": code, + 'headers': { + "Access-Control-Allow-Origin": '*', + "Content-Type": "application/json", + "Referrer-Policy": "origin" + }, + "multiValueHeaders": {"Access-Control-Allow-Methods": ["POST", "OPTIONS", "GET", "PATCH", "DELETE"]}, + "body": body + } + + +def resp_handler(func): + def handle(*args, **kwargs): + try: + resp = func(*args, **kwargs) + return api_response(200, resp) + except LookupError: + resp = JsonPayloadBuilder().add_status(False).add_data(None) \ + .add_message("Not found").compile() + return api_response(404, resp) + except Exception as e: + logging.error(str(e)) + resp = JsonPayloadBuilder().add_status(False).add_data(None) \ + .add_message("Internal error, please contact bugs@wasedatime.com.").compile() + return api_response(500, resp) + + return handle + + +def generate_url(bucket_name, object_key, expiration=3600): + try: + response = s3_client.generate_presigned_url('get_object', + Params={'Bucket': bucket_name, + 'Key': object_key}, + ExpiresIn=expiration) + except Exception as e: + logging.error(str(e)) + return None + + return response diff --git a/src/lambda/test-post-thread/index.py b/src/lambda/test-post-thread/index.py index cd3be3447..459150b6e 100644 --- a/src/lambda/test-post-thread/index.py +++ b/src/lambda/test-post-thread/index.py @@ -1,7 +1,7 @@ from boto3.dynamodb.conditions import Key import json from datetime import datetime -from utils import JsonPayloadBuilder, table, resp_handler, build_thread_id, s3_client, bucket +from utils import JsonPayloadBuilder, table, resp_handler, build_thread_id, s3_client, bucket, sanitize_title import uuid import base64 @@ -24,7 +24,8 @@ def test_post_thread(thread, uid): raise ValueError("Invalid content type") # Extracts 'jpeg', 'png', or 'gif' from the MIME type extension = content_type.split("/")[-1] - object_key = f"{thread_id}/image.{extension}" + sanitized_title = sanitize_title(thread["title"]) + object_key = f"{thread_id}/{sanitized_title}.{extension}" s3_client.put_object(Bucket=bucket, Key=object_key, Body=image_data, ContentType=content_type)