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]
|
[dev-dependencies]
|
||||||
tempfile = "3"
|
tempfile = "3"
|
||||||
|
surf = { version = "2", default-features = false, features = ["h1-client"] }
|
||||||
|
@ -24,6 +24,7 @@ Voir aussi:
|
|||||||
- `depannage.md`
|
- `depannage.md`
|
||||||
- `performance.md`
|
- `performance.md`
|
||||||
- `api_json_spec.md`
|
- `api_json_spec.md`
|
||||||
|
- `api_contrats.md`
|
||||||
|
|
||||||
## REX technique
|
## 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 async_std::stream::StreamExt;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use std::time::{Duration, SystemTime, UNIX_EPOCH};
|
use std::time::{Duration, SystemTime, UNIX_EPOCH};
|
||||||
use tide::StatusCode;
|
use tide::{StatusCode, Request, Response};
|
||||||
|
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug)]
|
||||||
pub struct StorageService {
|
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 {
|
pub fn unix_to_system_time(unix_timestamp: u64) -> SystemTime {
|
||||||
UNIX_EPOCH + Duration::from_secs(unix_timestamp)
|
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 std::env;
|
||||||
use async_std::task;
|
use async_std::task;
|
||||||
use async_std::fs::create_dir_all;
|
use async_std::fs::create_dir_all;
|
||||||
use tide::{Request, Response, StatusCode};
|
use sdk_storage::{StorageService, create_app};
|
||||||
use sdk_storage::StorageService;
|
|
||||||
|
|
||||||
const STORAGE_DIR: &str = "./storage";
|
const STORAGE_DIR: &str = "./storage";
|
||||||
const PORT: u16 = 8081;
|
const PORT: u16 = 8081;
|
||||||
const MIN_TTL: u64 = 60;
|
|
||||||
const DEFAULT_TTL: u64 = 86400;
|
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_std::main]
|
||||||
async fn main() -> tide::Result<()> {
|
async fn main() -> tide::Result<()> {
|
||||||
@ -150,9 +34,7 @@ async fn main() -> tide::Result<()> {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
let mut app = tide::with_state(svc);
|
let mut app = create_app(no_ttl_permanent, STORAGE_DIR);
|
||||||
app.at("/store").post(move |req| handle_store(req, no_ttl_permanent));
|
|
||||||
app.at("/retrieve/:key").get(handle_retrieve);
|
|
||||||
app.listen(format!("0.0.0.0:{}", PORT)).await?;
|
app.listen(format!("0.0.0.0:{}", PORT)).await?;
|
||||||
|
|
||||||
println!("Server running at http://0.0.0.0:{}", PORT);
|
println!("Server running at http://0.0.0.0:{}", PORT);
|
||||||
|
Loading…
x
Reference in New Issue
Block a user