test(api): couverture complète endpoints + docs contrats
Some checks failed
Docker Image / docker (push) Failing after 18s
CI / rust (push) Failing after 30s
Release / build-release (ubuntu-latest) (push) Failing after 57s
Release / build-release (windows-latest) (push) Has been cancelled

This commit is contained in:
Your Name 2025-08-26 10:34:11 +02:00
parent 0950da48d6
commit 62a972594d
5 changed files with 134 additions and 121 deletions

View File

@ -12,3 +12,4 @@ hex = "0.4"
[dev-dependencies] [dev-dependencies]
tempfile = "3" tempfile = "3"
surf = { version = "2", default-features = false, features = ["h1-client"] }

View File

@ -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
View 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 31536000; par défaut 86400 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.

View File

@ -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
}

View File

@ -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);