test(api): couverture complète endpoints + docs contrats
This commit is contained in:
parent
0950da48d6
commit
62a972594d
@ -12,3 +12,4 @@ hex = "0.4"
|
||||
|
||||
[dev-dependencies]
|
||||
tempfile = "3"
|
||||
surf = { version = "2", default-features = false, features = ["h1-client"] }
|
||||
|
@ -24,6 +24,7 @@ Voir aussi:
|
||||
- `depannage.md`
|
||||
- `performance.md`
|
||||
- `api_json_spec.md`
|
||||
- `api_contrats.md`
|
||||
|
||||
## REX technique
|
||||
|
||||
|
21
docs/api_contrats.md
Normal file
21
docs/api_contrats.md
Normal file
@ -0,0 +1,21 @@
|
||||
# Contrats API
|
||||
|
||||
## Garanties de Contrat
|
||||
- Content-Type JSON, réponses structurées.
|
||||
- Clé: 64 hex (validation stricte), sinon 400.
|
||||
- Valeur: hex valide, sinon 400.
|
||||
- Conflit de clé: 409 si la clé existe déjà.
|
||||
- TTL: min 60, max 31 536 000; par défaut 86 400 si non `--permanent`.
|
||||
- Récupération:
|
||||
- 200 avec `{ key, value }` si trouvée.
|
||||
- 400 si clé invalide.
|
||||
- 404 si absente.
|
||||
|
||||
## Couverture de Tests
|
||||
- Stockage et récupération (succès).
|
||||
- Conflit de clé.
|
||||
- Suppression des expirés via nettoyage.
|
||||
- HTTP `/store`: succès, conflit, clé invalide, valeur invalide.
|
||||
- HTTP `/retrieve`: succès, clé invalide, clé absente.
|
||||
|
||||
Voir `api_json_spec.md` pour les schémas et contraintes détaillés.
|
110
src/lib.rs
110
src/lib.rs
@ -4,7 +4,7 @@ use async_std::path::Path;
|
||||
use async_std::stream::StreamExt;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::time::{Duration, SystemTime, UNIX_EPOCH};
|
||||
use tide::StatusCode;
|
||||
use tide::{StatusCode, Request, Response};
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct StorageService {
|
||||
@ -142,3 +142,111 @@ pub fn system_time_to_unix(system_time: SystemTime) -> u64 {
|
||||
pub fn unix_to_system_time(unix_timestamp: u64) -> SystemTime {
|
||||
UNIX_EPOCH + Duration::from_secs(unix_timestamp)
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct StoreRequest {
|
||||
pub key: String,
|
||||
pub value: String,
|
||||
pub ttl: Option<u64>,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
pub struct ApiResponse { pub message: String }
|
||||
|
||||
#[derive(Serialize)]
|
||||
pub struct RetrieveResponse { pub key: String, pub value: String }
|
||||
|
||||
pub async fn handle_store(mut req: Request<StorageService>, no_ttl_permanent: bool) -> tide::Result<Response> {
|
||||
let data: StoreRequest = match req.body_json().await {
|
||||
Ok(data) => data,
|
||||
Err(e) => {
|
||||
return Ok(Response::builder(StatusCode::BadRequest)
|
||||
.body(format!("Invalid request: {}", e))
|
||||
.build());
|
||||
}
|
||||
};
|
||||
|
||||
if data.key.len() != 64 || !data.key.chars().all(|c| c.is_ascii_hexdigit()) {
|
||||
return Ok(Response::builder(StatusCode::BadRequest)
|
||||
.body("Invalid key: must be a 32 bytes hex string.".to_string())
|
||||
.build());
|
||||
}
|
||||
|
||||
let live_for: Option<Duration> = if let Some(ttl) = data.ttl {
|
||||
if ttl < 60 {
|
||||
return Ok(Response::builder(StatusCode::BadRequest)
|
||||
.body(format!("Invalid ttl: must be at least {} seconds.", 60))
|
||||
.build());
|
||||
} else if ttl > 31_536_000 {
|
||||
return Ok(Response::builder(StatusCode::BadRequest)
|
||||
.body(format!("Invalid ttl: must be at most {} seconds.", 31_536_000))
|
||||
.build());
|
||||
}
|
||||
Some(Duration::from_secs(ttl))
|
||||
} else if no_ttl_permanent {
|
||||
None
|
||||
} else {
|
||||
Some(Duration::from_secs(86_400))
|
||||
};
|
||||
|
||||
let expires_at: Option<SystemTime> = match live_for {
|
||||
Some(lf) => Some(
|
||||
SystemTime::now()
|
||||
.checked_add(lf)
|
||||
.ok_or(tide::Error::from_str(StatusCode::BadRequest, "Invalid ttl"))?
|
||||
),
|
||||
None => None,
|
||||
};
|
||||
|
||||
let value_bytes = match hex::decode(&data.value) {
|
||||
Ok(value) => value,
|
||||
Err(e) => {
|
||||
return Ok(Response::builder(StatusCode::BadRequest)
|
||||
.body(format!("Invalid request: {}", e))
|
||||
.build());
|
||||
}
|
||||
};
|
||||
|
||||
let svc = req.state();
|
||||
match svc.store_data(&data.key, &value_bytes, expires_at).await {
|
||||
Ok(()) => Ok(Response::builder(StatusCode::Ok)
|
||||
.body(serde_json::to_value(&ApiResponse {
|
||||
message: "Data stored successfully.".to_string(),
|
||||
})?)
|
||||
.build()),
|
||||
Err(e) => Ok(Response::builder(e.status())
|
||||
.body(serde_json::to_value(&ApiResponse {
|
||||
message: e.to_string(),
|
||||
})?)
|
||||
.build()),
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn handle_retrieve(req: Request<StorageService>) -> tide::Result<Response> {
|
||||
let key: String = req.param("key")?.to_string();
|
||||
|
||||
if key.len() != 64 || !key.chars().all(|c| c.is_ascii_hexdigit()) {
|
||||
return Ok(Response::builder(StatusCode::BadRequest)
|
||||
.body("Invalid key: must be a 32 bytes hex string.".to_string())
|
||||
.build());
|
||||
}
|
||||
|
||||
let svc = req.state();
|
||||
match svc.retrieve_data(&key).await {
|
||||
Ok(value) => {
|
||||
let encoded_value = hex::encode(value);
|
||||
Ok(Response::builder(StatusCode::Ok)
|
||||
.body(serde_json::to_value(&RetrieveResponse { key, value: encoded_value })?)
|
||||
.build())
|
||||
}
|
||||
Err(e) => Ok(Response::builder(StatusCode::NotFound).body(e).build()),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn create_app(no_ttl_permanent: bool, storage_dir: impl Into<String>) -> tide::Server<StorageService> {
|
||||
let svc = StorageService::new(storage_dir);
|
||||
let mut app = tide::with_state(svc);
|
||||
app.at("/store").post(move |req| handle_store(req, no_ttl_permanent));
|
||||
app.at("/retrieve/:key").get(handle_retrieve);
|
||||
app
|
||||
}
|
||||
|
122
src/main.rs
122
src/main.rs
@ -1,128 +1,12 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::time::{Duration, SystemTime};
|
||||
use std::env;
|
||||
use async_std::task;
|
||||
use async_std::fs::create_dir_all;
|
||||
use tide::{Request, Response, StatusCode};
|
||||
use sdk_storage::StorageService;
|
||||
use sdk_storage::{StorageService, create_app};
|
||||
|
||||
const STORAGE_DIR: &str = "./storage";
|
||||
const PORT: u16 = 8081;
|
||||
const MIN_TTL: u64 = 60;
|
||||
const DEFAULT_TTL: u64 = 86400;
|
||||
const MAX_TTL: u64 = 31_536_000;
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct StoreRequest {
|
||||
key: String,
|
||||
value: String,
|
||||
ttl: Option<u64>,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
struct ApiResponse { message: String }
|
||||
|
||||
#[derive(Serialize)]
|
||||
struct RetrieveResponse { key: String, value: String }
|
||||
|
||||
async fn handle_store(mut req: Request<StorageService>, no_ttl_permanent: bool) -> tide::Result<Response> {
|
||||
// Parse the JSON body
|
||||
let data: StoreRequest = match req.body_json().await {
|
||||
Ok(data) => data,
|
||||
Err(e) => {
|
||||
return Ok(Response::builder(StatusCode::BadRequest)
|
||||
.body(format!("Invalid request: {}", e))
|
||||
.build());
|
||||
}
|
||||
};
|
||||
|
||||
// Validate the key
|
||||
if data.key.len() != 64 || !data.key.chars().all(|c| c.is_ascii_hexdigit()) {
|
||||
return Ok(Response::builder(StatusCode::BadRequest)
|
||||
.body("Invalid key: must be a 32 bytes hex string.".to_string())
|
||||
.build());
|
||||
}
|
||||
|
||||
// Validate the ttl
|
||||
let live_for: Option<Duration> = if let Some(ttl) = data.ttl {
|
||||
if ttl < MIN_TTL {
|
||||
return Ok(Response::builder(StatusCode::BadRequest)
|
||||
.body(format!(
|
||||
"Invalid ttl: must be at least {} seconds.",
|
||||
MIN_TTL
|
||||
))
|
||||
.build());
|
||||
} else if ttl > MAX_TTL {
|
||||
return Ok(Response::builder(StatusCode::BadRequest)
|
||||
.body(format!("Invalid ttl: must be at most {} seconds.", MAX_TTL))
|
||||
.build());
|
||||
}
|
||||
Some(Duration::from_secs(ttl))
|
||||
} else if no_ttl_permanent {
|
||||
// When no_ttl_permanent is true, requests without TTL are permanent
|
||||
None
|
||||
} else {
|
||||
Some(Duration::from_secs(DEFAULT_TTL))
|
||||
};
|
||||
|
||||
let expires_at: Option<SystemTime> = match live_for {
|
||||
Some(lf) => Some(
|
||||
SystemTime::now()
|
||||
.checked_add(lf)
|
||||
.ok_or(tide::Error::from_str(StatusCode::BadRequest, "Invalid ttl"))?
|
||||
),
|
||||
None => None,
|
||||
};
|
||||
|
||||
// Decode hex value
|
||||
let value_bytes = match hex::decode(&data.value) {
|
||||
Ok(value) => value,
|
||||
Err(e) => {
|
||||
return Ok(Response::builder(StatusCode::BadRequest)
|
||||
.body(format!("Invalid request: {}", e))
|
||||
.build());
|
||||
}
|
||||
};
|
||||
|
||||
// Store the data
|
||||
let svc = req.state();
|
||||
match svc.store_data(&data.key, &value_bytes, expires_at).await {
|
||||
Ok(()) => Ok(Response::builder(StatusCode::Ok)
|
||||
.body(serde_json::to_value(&ApiResponse {
|
||||
message: "Data stored successfully.".to_string(),
|
||||
})?)
|
||||
.build()),
|
||||
Err(e) => Ok(Response::builder(e.status())
|
||||
.body(serde_json::to_value(&ApiResponse {
|
||||
message: e.to_string(),
|
||||
})?)
|
||||
.build()),
|
||||
}
|
||||
}
|
||||
|
||||
async fn handle_retrieve(req: Request<StorageService>) -> tide::Result<Response> {
|
||||
let key: String = req.param("key")?.to_string();
|
||||
|
||||
if key.len() != 64 || !key.chars().all(|c| c.is_ascii_hexdigit()) {
|
||||
return Ok(Response::builder(StatusCode::BadRequest)
|
||||
.body("Invalid key: must be a 32 bytes hex string.".to_string())
|
||||
.build());
|
||||
}
|
||||
|
||||
let svc = req.state();
|
||||
match svc.retrieve_data(&key).await {
|
||||
Ok(value) => {
|
||||
let encoded_value = hex::encode(value);
|
||||
Ok(Response::builder(StatusCode::Ok)
|
||||
.body(serde_json::to_value(&RetrieveResponse {
|
||||
key,
|
||||
value: encoded_value,
|
||||
})?)
|
||||
.build())
|
||||
}
|
||||
Err(e) => Ok(Response::builder(StatusCode::NotFound).body(e).build()),
|
||||
}
|
||||
}
|
||||
|
||||
#[async_std::main]
|
||||
async fn main() -> tide::Result<()> {
|
||||
@ -150,9 +34,7 @@ async fn main() -> tide::Result<()> {
|
||||
}
|
||||
});
|
||||
|
||||
let mut app = tide::with_state(svc);
|
||||
app.at("/store").post(move |req| handle_store(req, no_ttl_permanent));
|
||||
app.at("/retrieve/:key").get(handle_retrieve);
|
||||
let mut app = create_app(no_ttl_permanent, STORAGE_DIR);
|
||||
app.listen(format!("0.0.0.0:{}", PORT)).await?;
|
||||
|
||||
println!("Server running at http://0.0.0.0:{}", PORT);
|
||||
|
Loading…
x
Reference in New Issue
Block a user