Skip to content

Commit a6ba2ac

Browse files
authored
Improve test coverage (#105)
* Add lots of tests * Move bin logic to lib * Fix clippy * Remove testing-plan doc
1 parent 72c13b7 commit a6ba2ac

19 files changed

+2239
-806
lines changed

src/api/handlers.rs

Lines changed: 611 additions & 0 deletions
Large diffs are not rendered by default.

src/api/mod.rs

Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
//! HTTP API handler for subscription management.
2+
//!
3+
//! Framework-agnostic: accepts `ApiRequest`, returns `ApiResponse`.
4+
//! The Lambda entry point in `src/bin/api.rs` adapts `lambda_http` types to/from
5+
//! these and calls `handle`.
6+
7+
mod handlers;
8+
9+
use crate::captcha::Captcha;
10+
use crate::mailer::Mailer;
11+
use crate::storage::Storage;
12+
use std::collections::HashMap;
13+
use std::sync::Arc;
14+
15+
// ============================================================================
16+
// Request / Response types
17+
// ============================================================================
18+
19+
pub struct ApiRequest {
20+
pub method: String,
21+
pub path: String,
22+
pub query: HashMap<String, String>,
23+
pub body: Option<String>,
24+
}
25+
26+
pub enum ApiResponse {
27+
Html(String),
28+
Json { status: u16, body: String },
29+
Text { status: u16, body: String },
30+
Redirect(String),
31+
}
32+
33+
impl ApiResponse {
34+
pub fn status(&self) -> u16 {
35+
match self {
36+
Self::Html(_) => 200,
37+
Self::Json { status, .. } => *status,
38+
Self::Text { status, .. } => *status,
39+
Self::Redirect(_) => 303,
40+
}
41+
}
42+
43+
pub fn redirect_location(&self) -> Option<&str> {
44+
if let Self::Redirect(loc) = self {
45+
Some(loc)
46+
} else {
47+
None
48+
}
49+
}
50+
51+
pub fn body_contains(&self, s: &str) -> bool {
52+
match self {
53+
Self::Html(body) | Self::Json { body, .. } | Self::Text { body, .. } => {
54+
body.contains(s)
55+
}
56+
Self::Redirect(_) => false,
57+
}
58+
}
59+
}
60+
61+
// ============================================================================
62+
// Application state
63+
// ============================================================================
64+
65+
pub struct AppState<S, M, C> {
66+
pub(crate) storage: Arc<S>,
67+
pub(crate) mailer: Arc<M>,
68+
pub(crate) captcha: C,
69+
pub(crate) base_url: String,
70+
}
71+
72+
impl<S, M, C> AppState<S, M, C> {
73+
pub fn new(storage: Arc<S>, mailer: Arc<M>, captcha: C, base_url: String) -> Self {
74+
Self {
75+
storage,
76+
mailer,
77+
captcha,
78+
base_url,
79+
}
80+
}
81+
}
82+
83+
// ============================================================================
84+
// Dispatch
85+
// ============================================================================
86+
87+
pub async fn handle<S, M, C>(request: &ApiRequest, state: &Arc<AppState<S, M, C>>) -> ApiResponse
88+
where
89+
S: Storage,
90+
M: Mailer,
91+
C: Captcha,
92+
{
93+
let token = request.query.get("token").map(|s| s.as_str()).unwrap_or("");
94+
let email = request.query.get("email").map(|s| s.as_str()).unwrap_or("");
95+
let body = request.body.as_deref().unwrap_or("");
96+
97+
match (request.method.as_str(), request.path.as_str()) {
98+
("POST", "/api/subscribe") => handlers::subscribe_post(state, body).await,
99+
("GET", "/api/verify") => handlers::verify_get(state, email, token).await,
100+
("GET", "/api/unsubscribe") => handlers::unsubscribe_get(&state.storage, token).await,
101+
("POST", "/api/unsubscribe") => {
102+
handlers::unsubscribe_post(&state.storage, token, request.body.as_deref()).await
103+
}
104+
_ => ApiResponse::Text {
105+
status: 404,
106+
body: "Not Found".to_string(),
107+
},
108+
}
109+
}

src/bin/add_subscriber.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
use anyhow::{Context, Result};
22
use aws_config::BehaviorVersion;
33
use email_address::EmailAddress;
4-
use hndigest::storage_adapter::StorageAdapter;
4+
use hndigest::storage::{LambdaStorage, Storage};
55
use hndigest::strategies::DigestStrategy;
66
use hndigest::types::Subscriber;
77
use std::env;
@@ -29,7 +29,7 @@ async fn main() -> Result<()> {
2929
println!("Initializing DynamoDB client...");
3030
let config = aws_config::load_defaults(BehaviorVersion::latest()).await;
3131
let dynamodb_client = aws_sdk_dynamodb::Client::new(&config);
32-
let storage_adapter = Arc::new(StorageAdapter::new(dynamodb_client, dynamodb_table));
32+
let storage_adapter = Arc::new(LambdaStorage::new(dynamodb_client, dynamodb_table));
3333

3434
println!("Creating subscriber for {}", email);
3535
let subscriber = Subscriber::new(email, strategy);

0 commit comments

Comments
 (0)