Skip to content

Commit

Permalink
feat: GET /v1/links support (#7)
Browse files Browse the repository at this point in the history
  • Loading branch information
kentSarmiento authored Dec 18, 2023
1 parent 3cd10c9 commit 6a4c880
Show file tree
Hide file tree
Showing 14 changed files with 378 additions and 50 deletions.
18 changes: 18 additions & 0 deletions .github/workflows/development.yml
Original file line number Diff line number Diff line change
Expand Up @@ -41,3 +41,21 @@ jobs:

- name: Clippy
run: cargo clippy --all-targets --all-features -- -D warnings

test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3

- name: Install Rust
uses: actions-rs/toolchain@v1
with:
toolchain: 1.74.0
profile: minimal
components: rustfmt, clippy

- name: Unit Test
run: cargo test --lib

- name: Integration Test
run: cargo test --test '*'
96 changes: 96 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 5 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,17 @@ publish = false

[dependencies]
axum = "0.7.2"
http-body-util = "0.1.0"
#lambda_http = "0.8.3"
lambda_http = { git = "https://github.com/awslabs/aws-lambda-rust-runtime", branch = "hyper1_upgrade" }
lambda_runtime = "0.8.3"
serde = { version = "1.0.193", features = [ "derive" ] }
serde_json = "1.0.108"
tokio = { version = "1", features = ["macros"] }
tower = "0.4.13"
tracing = { version = "0.1", features = ["log"] }
tracing-subscriber = { version = "0.3", default-features = false, features = ["fmt"] }

[dev-dependencies]
mockall = "0.12.0"

16 changes: 14 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,14 +31,26 @@ Setup:

Testing:

1. [cargo lambda](https://www.cargo-lambda.info/) is used for development of this service and it is pre-installed as part of the devcontainer. Use [cargo lambda watch](https://www.cargo-lambda.info/commands/watch.html) to hotcompile your changes:
1. [`cargo lambda`](https://www.cargo-lambda.info/) is used for development of this service and it is pre-installed as part of the devcontainer. Use [`cargo lambda watch`](https://www.cargo-lambda.info/commands/watch.html) to hotcompile your changes:

```sh
cargo lambda watch
```

1. [cargo clippy](https://github.com/rust-lang/rust-clippy) is used for linting to catch common errors. This is setup to run on saving changes in the devcontainer. You may also run it from bash using the following command:
1. [`cargo clippy`](https://github.com/rust-lang/rust-clippy) is used for linting to catch common errors. This is setup to run on saving changes in the devcontainer. You may also run it from bash using the following command:

```sh
cargo clippy --all-targets --all-features -- -D warnings
```

1. `cargo test` is used to run unit/integration tests

Unit Test:
```sh
cargo test --lib
```

Integration Test:
```sh
cargo test --test '*'
```
85 changes: 80 additions & 5 deletions src/controller/links.rs
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ async fn list(State(app_state): State<RouterState>) -> impl IntoResponse {
tracing::error!("Error: {}", e);
(
StatusCode::INTERNAL_SERVER_ERROR,
Json(json!({ "msg": e.to_string() })),
Json(json!({ "msg": "An error occurred." })),
)
.into_response()
}
Expand All @@ -43,7 +43,7 @@ async fn post(State(app_state): State<RouterState>) -> impl IntoResponse {
tracing::error!("Error: {}", e);
(
StatusCode::INTERNAL_SERVER_ERROR,
Json(json!({ "msg": e.to_string() })),
Json(json!({ "msg": "An error occurred." })),
)
.into_response()
}
Expand All @@ -58,7 +58,7 @@ async fn get(Path(id): Path<String>, State(app_state): State<RouterState>) -> im
tracing::error!("Error: {}", e);
(
StatusCode::INTERNAL_SERVER_ERROR,
Json(json!({ "msg": e.to_string() })),
Json(json!({ "msg": "An error occurred." })),
)
.into_response()
}
Expand All @@ -73,7 +73,7 @@ async fn put(Path(id): Path<String>, State(app_state): State<RouterState>) -> im
tracing::error!("Error: {}", e);
(
StatusCode::INTERNAL_SERVER_ERROR,
Json(json!({ "msg": e.to_string() })),
Json(json!({ "msg": "An error occurred." })),
)
.into_response()
}
Expand All @@ -88,9 +88,84 @@ async fn delete(Path(id): Path<String>, State(app_state): State<RouterState>) ->
tracing::error!("Error: {}", e);
(
StatusCode::INTERNAL_SERVER_ERROR,
Json(json!({ "msg": e.to_string() })),
Json(json!({ "msg": "An error occurred." })),
)
.into_response()
}
}
}

#[cfg(test)]
mod tests {
use std::sync::Arc;

use http_body_util::BodyExt;

use crate::types::{
links::LinkItem, repository::MockLinks as MockRepository, service::MockLinks as MockService,
};

use super::*;

#[tokio::test]
async fn test_get_links_empty() {
let mut mock_links_service = MockService::new();
let mock_links_repo = MockRepository::new();
mock_links_service
.expect_list()
.times(1)
.returning(|_| Ok(vec![]));

let app_state = RouterState::new(Arc::new(mock_links_service), Arc::new(mock_links_repo));

let response = list(State(app_state)).await;

let (parts, body) = response.into_response().into_parts();
assert_eq!(StatusCode::OK, parts.status);

let body = body.collect().await.unwrap().to_bytes();
assert_eq!(&body[..], b"[]");
}

#[tokio::test]
async fn test_get_links_non_empty() {
let mut mock_links_service = MockService::new();
let mock_links_repo = MockRepository::new();
mock_links_service
.expect_list()
.times(1)
.returning(|_| Ok(vec![LinkItem::new("http://link")]));

let app_state = RouterState::new(Arc::new(mock_links_service), Arc::new(mock_links_repo));

let response = list(State(app_state)).await;

let (parts, body) = response.into_response().into_parts();
assert_eq!(StatusCode::OK, parts.status);

let body = body.collect().await.unwrap().to_bytes();
let body = std::str::from_utf8(&body).unwrap();
assert!(body.contains("http://link"));
}

#[tokio::test]
async fn test_get_links_service_error() {
let mut mock_links_service = MockService::new();
let mock_links_repo = MockRepository::new();
mock_links_service
.expect_list()
.times(1)
.returning(|_| Err("A service error occurred.".into()));

let app_state = RouterState::new(Arc::new(mock_links_service), Arc::new(mock_links_repo));

let response = list(State(app_state)).await;

let (parts, body) = response.into_response().into_parts();
assert_eq!(StatusCode::INTERNAL_SERVER_ERROR, parts.status);

let body = body.collect().await.unwrap().to_bytes();
let body = std::str::from_utf8(&body).unwrap();
assert!(body.contains("An error occurred."));
}
}
16 changes: 16 additions & 0 deletions src/repository/links.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,4 +12,20 @@ impl Links for Repository {
async fn list(&self) -> Result<Vec<LinkItem>> {
Ok(vec![])
}

async fn post(&self) -> Result<LinkItem> {
Err("Not implemented".into())
}

async fn get(&self, _id: &str) -> Result<LinkItem> {
Err("Not implemented".into())
}

async fn put(&self, _id: &str) -> Result<LinkItem> {
Err("Not implemented".into())
}

async fn delete(&self, _id: &str) -> Result<()> {
Err("Not implemented".into())
}
}
15 changes: 10 additions & 5 deletions src/router.rs
Original file line number Diff line number Diff line change
@@ -1,16 +1,21 @@
use std::sync::Arc;

use axum::Router;

use crate::{
controller, repository, service,
types::{repository::DynLinks as DynLinksRepo, service::DynLinks as DynLinksService, state},
types::{
repository::DynLinks as DynLinksRepo, service::DynLinks as DynLinksService,
state::Router as RouterState,
},
};

pub fn new() -> axum::Router {
let links_repo = Arc::new(repository::links::Repository {}) as DynLinksRepo;
pub fn new() -> Router {
let links_service = Arc::new(service::links::Service {}) as DynLinksService;
let links_repo = Arc::new(repository::links::Repository {}) as DynLinksRepo;

let state = state::Router::new(links_repo, links_service);
axum::Router::new()
let state = RouterState::new(links_service, links_repo);
Router::new()
.merge(controller::links::router())
.with_state(state)
}
Loading

0 comments on commit 6a4c880

Please sign in to comment.