Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Feature] Put Recipe #77

Merged
merged 8 commits into from
Sep 21, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
204 changes: 115 additions & 89 deletions Cargo.lock

Large diffs are not rendered by default.

249 changes: 123 additions & 126 deletions package-lock.json

Large diffs are not rendered by default.

14 changes: 7 additions & 7 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,24 +14,24 @@
"watch": "tsc -w"
},
"devDependencies": {
"@playwright/test": "^1.46.1",
"@types/jest": "^29.5.12",
"@types/node": "^20.16.1",
"@playwright/test": "^1.47.2",
"@types/jest": "^29.5.13",
"@types/node": "^20.16.5",
"@typescript-eslint/eslint-plugin": "^6.21.0",
"@typescript-eslint/parser": "^6.21.0",
"aws-cdk": "^2.154.1",
"eslint": "^8.57.0",
"aws-cdk": "^2.159.1",
"eslint": "^8.57.1",
"eslint-config-google": "^0.14.0",
"jest": "^29.7.0",
"ts-jest": "^29.2.5",
"ts-node": "^10.9.2",
"typescript": "~5.2.2"
},
"dependencies": {
"aws-cdk-lib": "^2.154.1",
"aws-cdk-lib": "^2.159.1",
"aws-cdk-local": "^2.18.0",
"cargo-lambda-cdk": "^0.0.22",
"cdk-pipelines-github": "^0.4.124",
"cdk-pipelines-github": "^0.4.125",
"constructs": "^10.3.0",
"source-map-support": "^0.5.21"
}
Expand Down
4 changes: 3 additions & 1 deletion playwright.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ import { defineConfig } from '@playwright/test';
* https://github.com/motdotla/dotenv
*/
// require('dotenv').config();
const baseUrl = process.env.PLAYWRIGHT_BASE_URL ??
'https://api.sandbox.mealplanner.chittyinsights.dev';

/**
* See https://playwright.dev/docs/test-configuration.
Expand All @@ -25,7 +27,7 @@ export default defineConfig({
* See https://playwright.dev/docs/api/class-testoptions.
*/
use: {
baseURL: `${process.env.PLAYWRIGHT_BASE_URL}`,
baseURL: `${baseUrl}`,
extraHTTPHeaders: {
'Accept': 'application/json',
// 'Authorization': `token ${process.env.API_TOKEN}`,
Expand Down
9 changes: 9 additions & 0 deletions playwright/tests/recipeConstants.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
export const NIL_UUID = '00000000-0000-0000-0000-000000000000';

export const createData = {
name: 'Playwright Recipe',
};

export const updateData = {
name: 'Updated Playwright Recipe',
};
41 changes: 41 additions & 0 deletions playwright/tests/recipeErrors.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import { test, expect } from '@playwright/test';
import { NIL_UUID, updateData } from './recipeConstants';

test('Create Invalid Recipe', async ({ request }) => {
const response = await request.put(`./recipes`, {
data: {
id: 'this-is-not-a-uuid',
name: '',
},
});

expect(response.status()).toEqual(422);
});

test('Create Recipe w/ Null Name', async ({ request }) => {
const response = await request.put(`./recipes`, {
data: {
id: NIL_UUID,
},
});

expect(response.status()).toEqual(422);
});

test('Read Non-existent Recipe', async ({ request }) => {
const response = await request.get(`./recipes/${NIL_UUID}`);

expect(response.status()).toEqual(404);
});

test('Update Non-existent Recipe', async ({ request }) => {
const response = await request.patch(`./recipes/${NIL_UUID}`, { data: updateData });

expect(response.status()).toEqual(404);
});

test('Delete Non-existent Recipe', async ({ request }) => {
const response = await request.delete(`./recipes/${NIL_UUID}`);

expect(response.status()).toEqual(404);
});
Original file line number Diff line number Diff line change
@@ -1,14 +1,6 @@
import { test, expect } from '@playwright/test';
import { createData, updateData } from './recipeConstants';

const createData = {
name: 'Playwright Recipe',
};

const updateData = {
name: 'Updated Playwright Recipe',
};

const NIL_UUID = '00000000-0000-0000-0000-000000000000';

let recipeUuid: string;

Expand All @@ -22,10 +14,10 @@ test('pingable', async ({ request }) => {
test.describe('Happy Path', () => {
test.describe.configure({ mode: 'serial' });

test('Create Recipe', async ({ request }) => {
test('Post Recipe', async ({ request }) => {
const response = await request.post('./recipes', { data: createData });

expect(response.ok()).toBeTruthy();
expect(response.status()).toBe(201);

const responseBody = await response.json();
expect(responseBody).toEqual({
Expand All @@ -36,6 +28,19 @@ test.describe('Happy Path', () => {
recipeUuid = responseBody.id;
});

test('Put Recipe', async ({ request }) => {
const response = await request.put('./recipes', { data: { id: recipeUuid, ...createData } });

expect(response.status()).toBe(200);

const responseBody = await response.json();
expect(responseBody).toEqual({
id: recipeUuid,
...createData,
});
recipeUuid = responseBody.id;
});

test('Read Recipe', async ({ request }) => {
const response = await request.get(`./recipes/${recipeUuid}`);

Expand Down Expand Up @@ -83,21 +88,3 @@ test.describe('Happy Path', () => {
expect(response.status()).toEqual(204);
});
});

test('Read Non-existent Recipe', async ({ request }) => {
const response = await request.get(`./recipes/${NIL_UUID}`);

expect(response.status()).toEqual(404);
});

test('Update Non-existent Recipe', async ({ request }) => {
const response = await request.patch(`./recipes/${NIL_UUID}`, { data: updateData });

expect(response.status()).toEqual(404);
});

test('Delete Non-existent Recipe', async ({ request }) => {
const response = await request.delete(`./recipes/${NIL_UUID}`);

expect(response.status()).toEqual(404);
});
92 changes: 92 additions & 0 deletions playwright/tests/recipesPut.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
import { test, expect } from '@playwright/test';
import { createData, updateData } from './recipeConstants';

let recipeUuid: string = '01010101-0101-0101-0101-010101010101';

test('pingable', async ({ request }) => {
const response = await request.get('./recipes/ping');

expect(response.ok()).toBeTruthy();
expect(await response.json()).toEqual({ msg: 'Pong' });
});

test.describe('Happy Path', () => {
test.describe.configure({ mode: 'serial' });

test('Put Recipe to create', async ({ request }) => {
const response = await request.put('./recipes', { data: { id: recipeUuid, ...createData } });

expect(response.status()).toBe(201);

const responseBody = await response.json();
expect(responseBody).toEqual({
id: recipeUuid,
...createData,
});
});

test('Read Recipe', async ({ request }) => {
const response = await request.get(`./recipes/${recipeUuid}`);

expect(response.ok()).toBeTruthy();
expect(await response.json()).toEqual({
id: recipeUuid,
...createData,
});
});

test('List Recipes', async ({ request }) => {
const response = await request.get('./recipes');

expect(response.ok()).toBeTruthy();
const json = await response.json();
expect(json).toContainEqual({
id: recipeUuid,
...createData,
});
});

test('Put Recipe update', async ({ request }) => {
const response = await request.put('./recipes', {
data: {
id: recipeUuid,
name: 'PUT Update',
},
});

expect(response.status()).toBe(200);

const responseBody = await response.json();
expect(responseBody).toEqual({
id: recipeUuid,
name: 'PUT Update',
});
recipeUuid = responseBody.id;
});

test('Update Recipe', async ({ request }) => {
const response = await request.patch(`./recipes/${recipeUuid}`, { data: updateData });

expect(response.ok()).toBeTruthy();
expect(await response.json()).toEqual({
id: recipeUuid,
...updateData,
});
});

test('Read Updated Recipe', async ({ request }) => {
const response = await request.get(`./recipes/${recipeUuid}`);

expect(response.ok()).toBeTruthy();
expect(await response.json()).toEqual({
id: recipeUuid,
...updateData,
});
});

test.afterAll('Delete Recipe', async ({ request }) => {
const response = await request.delete(`./recipes/${recipeUuid}`);

expect(response.status()).toEqual(204);
});
});
3 changes: 2 additions & 1 deletion src/bin/recipes/controller.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
use aws_config::BehaviorVersion;
use axum::routing::{delete, get, patch, post};
use axum::routing::{delete, get, patch, post, put};
use axum::Router;
use meal_planner::recipe::repository::DynamoDbRecipe;
use meal_planner::services::{recipes, ApplicationContext};
Expand All @@ -19,6 +19,7 @@ pub async fn recipes() -> Router {
Router::new()
.route("/", get(recipes::list::<DynamoDbRecipe>))
.route("/", post(recipes::create::<DynamoDbRecipe>))
.route("/", put(recipes::write::<DynamoDbRecipe>))
.route("/:id", get(recipes::read_one::<DynamoDbRecipe>))
.route("/:id", patch(recipes::update::<DynamoDbRecipe>))
.route("/:id", delete(recipes::delete_one::<DynamoDbRecipe>))
Expand Down
3 changes: 2 additions & 1 deletion src/lib/aws_client/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ use aws_sdk_dynamodb::operation::delete_item::{DeleteItemError, DeleteItemOutput
use aws_sdk_dynamodb::operation::get_item::{GetItemError, GetItemOutput};
use aws_sdk_dynamodb::operation::put_item::{PutItemError, PutItemOutput};
use aws_sdk_dynamodb::operation::scan::{ScanError, ScanOutput};
use aws_sdk_dynamodb::types::AttributeValue;
use aws_sdk_dynamodb::types::{AttributeValue, ReturnValue};
use aws_sdk_dynamodb::Client;
use axum::async_trait;

Expand Down Expand Up @@ -67,6 +67,7 @@ impl DynamoDbClient for DynamoDbClientImpl {
.put_item()
.table_name(table_name)
.set_item(Some(item))
.return_values(ReturnValue::AllOld)
.send()
.await
.map_err(SdkError::into_service_error)
Expand Down
2 changes: 1 addition & 1 deletion src/lib/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,6 @@ pub mod services;
pub trait Repository<T: Send + Sync>: Send + Sync {
fn get_all(&self) -> impl Future<Output = Result<Vec<T>, StatusCode>> + Send;
fn find_by_id(&self, id: Uuid) -> impl Future<Output = Result<T, StatusCode>> + Send;
fn save(&self, item: &T) -> impl Future<Output = Result<(), StatusCode>> + Send;
fn save(&self, item: &T) -> impl Future<Output = Result<Option<T>, StatusCode>> + Send;
fn delete_by_id(&self, id: Uuid) -> impl Future<Output = Result<(), StatusCode>> + Send;
}
14 changes: 11 additions & 3 deletions src/lib/recipe/mapper.rs
Original file line number Diff line number Diff line change
@@ -1,16 +1,24 @@
use uuid::Uuid;

use super::request_models::{PatchRecipe, PostRecipe};
use super::request_models::{PatchRecipe, PostRecipe, PutRecipe};
use super::Recipe;

#[must_use]
pub fn to_recipe(id: Uuid, value: &PostRecipe) -> Recipe {
pub fn map_post_recipe(id: Uuid, value: &PostRecipe) -> Recipe {
Recipe {
id,
name: value.name.clone(),
}
}

#[must_use]
pub fn map_put_recipe(value: &PutRecipe) -> Recipe {
Recipe {
id: value.id,
name: value.name.clone(),
}
}

pub fn update_recipe(recipe: &mut Recipe, value: &PatchRecipe) {
if let Some(new_name) = &value.name {
recipe.name = new_name.to_string();
Expand All @@ -34,7 +42,7 @@ mod test {
name: NAME.to_owned(),
};

let recipe = mapper::to_recipe(Uuid::nil(), &create_request);
let recipe = mapper::map_post_recipe(Uuid::nil(), &create_request);

assert_eq!(
Recipe {
Expand Down
13 changes: 8 additions & 5 deletions src/lib/recipe/repository.rs
Original file line number Diff line number Diff line change
Expand Up @@ -48,8 +48,7 @@ impl Repository<Recipe> for DynamoDbRecipe {
let items: Vec<Recipe> = scan_result
.items()
.iter()
.map(|item| from_item(item.clone()))
.flatten()
.flat_map(|item| from_item(item.clone()))
.collect();

Ok(items)
Expand All @@ -74,14 +73,18 @@ impl Repository<Recipe> for DynamoDbRecipe {
Ok(recipe)
}

async fn save(&self, recipe: &Recipe) -> Result<(), StatusCode> {
async fn save(&self, recipe: &Recipe) -> Result<Option<Recipe>, StatusCode> {
let item = to_item(recipe).map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
self.client
let output = self
.client
.put_item(&self.table_name, item)
.await
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;

Ok(())
match output.attributes {
Some(item) => from_item(item).map_err(|_| StatusCode::INTERNAL_SERVER_ERROR),
None => Ok(None),
}
}

async fn delete_by_id(&self, id: Uuid) -> Result<(), StatusCode> {
Expand Down
7 changes: 7 additions & 0 deletions src/lib/recipe/request_models.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
use serde::{Deserialize, Deserializer};
use uuid::Uuid;

#[derive(Debug, Deserialize, PartialEq)]
pub struct PostRecipe {
Expand Down Expand Up @@ -27,6 +28,12 @@ where
Ok(option.filter(|s| !s.is_empty()))
}

#[derive(Debug, Deserialize, PartialEq)]
pub struct PutRecipe {
pub(super) id: Uuid,
pub(super) name: String,
}

#[cfg(test)]
mod test {
use super::PostRecipe;
Expand Down
Loading
Loading