Skip to content

Commit

Permalink
[Feature] Put Recipe (#77)
Browse files Browse the repository at this point in the history
* Dependency updates

* Update playwright base url default to sandbox

* Add put recipe w/o validation

* Reorganize playwright tests

* Fix issue where body of put response was old

* Add validation tests to playwright

* Clipp and formatting

* More formatting
  • Loading branch information
NChitty authored Sep 21, 2024
1 parent c2be45f commit ec87bdb
Show file tree
Hide file tree
Showing 15 changed files with 465 additions and 268 deletions.
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

0 comments on commit ec87bdb

Please sign in to comment.