feat: implement health statistics tracking (Phase 2.7 Task 2)

- Add HealthStatistics model with 10 stat types
- Implement HealthStatisticsRepository
- Create 6 health stats API endpoints
- Add trend analysis with summary calculations
- Follow medication repository pattern

Status: 60% complete, needs compilation fixes
This commit is contained in:
goose 2026-03-07 16:24:18 -03:00
parent d673415bc6
commit b59be78e4a
18 changed files with 2420 additions and 7 deletions

62
backend/deploy-and-test.sh Executable file
View file

@ -0,0 +1,62 @@
#!/bin/bash
echo "=========================================="
echo "Normogen Deployment & Testing Script"
echo "=========================================="
echo ""
# Step 1: Push to remote
echo "Step 1: Pushing to remote..."
git push origin main
if [ $? -ne 0 ]; then
echo "✗ Git push failed"
exit 1
fi
echo "✓ Git push successful"
echo ""
# Step 2: Connect to Solaria and update
echo "Step 2: Updating Solaria..."
ssh solaria 'cd /srv/normogen && git pull'
if [ $? -ne 0 ]; then
echo "✗ Git pull on Solaria failed"
exit 1
fi
echo "✓ Code updated on Solaria"
echo ""
# Step 3: Build new container
echo "Step 3: Building Docker container..."
ssh solaria 'cd /srv/normogen && docker compose -f docker/docker-compose.improved.yml build backend'
if [ $? -ne 0 ]; then
echo "✗ Docker build failed"
exit 1
fi
echo "✓ Docker build successful"
echo ""
# Step 4: Restart containers
echo "Step 4: Restarting containers..."
ssh solaria 'cd /srv/normogen && docker compose -f docker/docker-compose.improved.yml down && docker compose -f docker/docker-compose.improved.yml up -d'
if [ $? -ne 0 ]; then
echo "✗ Container restart failed"
exit 1
fi
echo "✓ Containers restarted"
echo ""
# Step 5: Wait for container to be healthy
echo "Step 5: Waiting for backend to be healthy..."
sleep 10
ssh solaria 'docker ps | grep normogen'
echo ""
# Step 6: Run API tests
echo "Step 6: Running API tests..."
chmod +x test-medication-endpoints.sh
./test-medication-endpoints.sh
echo ""
echo "=========================================="
echo "Deployment & Testing Complete"
echo "=========================================="

View file

@ -10,6 +10,8 @@ pub struct AppState {
pub audit_logger: Option<crate::security::AuditLogger>,
pub session_manager: Option<crate::security::SessionManager>,
pub account_lockout: Option<crate::security::AccountLockout>,
pub health_stats_repo: Option<crate::models::health_stats::HealthStatisticsRepository>,
pub mongo_client: Option<mongodb::Client>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]

View file

@ -0,0 +1,267 @@
use axum::{
extract::{Path, Query, State},
http::StatusCode,
Json,
};
use mongodb::bson::oid::ObjectId;
use serde::{Deserialize, Serialize};
use crate::models::health_stats::{
CreateHealthStatRequest, HealthStatistic, HealthStatType, HealthStatValue,
HealthStatisticsRepository, UpdateHealthStatRequest,
};
use crate::auth::jwt::Claims;
#[derive(Debug, Deserialize)]
pub struct ListStatsQuery {
pub stat_type: Option<String>,
pub profile_id: Option<String>,
pub limit: Option<i64>,
}
#[derive(Debug, Deserialize)]
pub struct TrendQuery {
pub profile_id: String,
pub stat_type: String,
pub days: Option<i64>,
}
#[derive(Debug, Serialize)]
pub struct TrendResponse {
pub stat_type: String,
pub profile_id: String,
pub days: i64,
pub data_points: i64,
pub stats: Vec<HealthStatistic>,
pub summary: TrendSummary,
}
#[derive(Debug, Serialize)]
pub struct TrendSummary {
pub latest: Option<f64>,
pub earliest: Option<f64>,
pub average: Option<f64>,
pub min: Option<f64>,
pub max: Option<f64>,
pub trend: String,
}
pub async fn create_health_stat(
State(repo): State<HealthStatisticsRepository>,
claims: Claims,
Json(req): Json<CreateHealthStatRequest>,
) -> Result<Json<HealthStatistic>, StatusCode> {
let stat_type = parse_stat_type(&req.stat_type);
let value = parse_stat_value(&req.value, &stat_type);
let unit = req.unit.unwrap_or_else(|| stat_type.default_unit().to_string());
let now = mongodb::bson::DateTime::now();
let health_stat_id = uuid::Uuid::new_v4().to_string();
let stat = HealthStatistic {
id: None,
health_stat_id,
user_id: claims.user_id.clone(),
profile_id: req.profile_id.clone(),
stat_type,
value,
unit,
recorded_at: req.recorded_at.unwrap_or(now),
notes: req.notes,
tags: req.tags.unwrap_or_default(),
created_at: now,
updated_at: now,
};
match repo.create(stat.clone()).await {
Ok(created) => Ok(Json(created)),
Err(_) => Err(StatusCode::INTERNAL_SERVER_ERROR),
}
}
pub async fn list_health_stats(
State(repo): State<HealthStatisticsRepository>,
claims: Claims,
Query(query): Query<ListStatsQuery>,
) -> Result<Json<Vec<HealthStatistic>>, StatusCode> {
let limit = query.limit.unwrap_or(100);
match repo
.list_by_user(
&claims.user_id,
query.stat_type.as_deref(),
query.profile_id.as_deref(),
limit,
)
.await
{
Ok(stats) => Ok(Json(stats)),
Err(_) => Err(StatusCode::INTERNAL_SERVER_ERROR),
}
}
pub async fn get_health_stat(
State(repo): State<HealthStatisticsRepository>,
claims: Claims,
Path(id): Path<String>,
) -> Result<Json<HealthStatistic>, StatusCode> {
match ObjectId::parse_str(&id) {
Ok(oid) => match repo.get_by_id(&oid, &claims.user_id).await {
Ok(Some(stat)) => Ok(Json(stat)),
Ok(None) => Err(StatusCode::NOT_FOUND),
Err(_) => Err(StatusCode::INTERNAL_SERVER_ERROR),
},
Err(_) => Err(StatusCode::BAD_REQUEST),
}
}
pub async fn update_health_stat(
State(repo): State<HealthStatisticsRepository>,
claims: Claims,
Path(id): Path<String>,
Json(req): Json<UpdateHealthStatRequest>,
) -> Result<Json<HealthStatistic>, StatusCode> {
match ObjectId::parse_str(&id) {
Ok(oid) => match repo.update(&oid, &claims.user_id, req).await {
Ok(Some(stat)) => Ok(Json(stat)),
Ok(None) => Err(StatusCode::NOT_FOUND),
Err(_) => Err(StatusCode::INTERNAL_SERVER_ERROR),
},
Err(_) => Err(StatusCode::BAD_REQUEST),
}
}
pub async fn delete_health_stat(
State(repo): State<HealthStatisticsRepository>,
claims: Claims,
Path(id): Path<String>,
) -> Result<StatusCode, StatusCode> {
match ObjectId::parse_str(&id) {
Ok(oid) => match repo.delete(&oid, &claims.user_id).await {
Ok(true) => Ok(StatusCode::NO_CONTENT),
Ok(false) => Err(StatusCode::NOT_FOUND),
Err(_) => Err(StatusCode::INTERNAL_SERVER_ERROR),
},
Err(_) => Err(StatusCode::BAD_REQUEST),
}
}
pub async fn get_health_trends(
State(repo): State<HealthStatisticsRepository>,
claims: Claims,
Query(query): Query<TrendQuery>,
) -> Result<Json<TrendResponse>, StatusCode> {
let days = query.days.unwrap_or(30);
match repo
.get_trends(&claims.user_id, &query.profile_id, &query.stat_type, days)
.await
{
Ok(stats) => {
let data_points = stats.len() as i64;
let summary = calculate_summary(&stats);
let response = TrendResponse {
stat_type: query.stat_type.clone(),
profile_id: query.profile_id,
days,
data_points,
stats,
summary,
};
Ok(Json(response))
}
Err(_) => Err(StatusCode::INTERNAL_SERVER_ERROR),
}
}
fn parse_stat_type(stat_type: &str) -> HealthStatType {
match stat_type.to_lowercase().as_str() {
"weight" => HealthStatType::Weight,
"height" => HealthStatType::Height,
"blood_pressure" => HealthStatType::BloodPressure,
"heart_rate" => HealthStatType::HeartRate,
"temperature" => HealthStatType::Temperature,
"blood_glucose" => HealthStatType::BloodGlucose,
"oxygen_saturation" => HealthStatType::OxygenSaturation,
"sleep_hours" => HealthStatType::SleepHours,
"steps" => HealthStatType::Steps,
"calories" => HealthStatType::Calories,
custom => HealthStatType::Custom(custom.to_string()),
}
}
fn parse_stat_value(value: &serde_json::Value, stat_type: &HealthStatType) -> HealthStatValue {
match stat_type {
HealthStatType::BloodPressure => {
if let Some(obj) = value.as_object() {
let systolic = obj.get("systolic").and_then(|v| v.as_f64()).unwrap_or(0.0);
let diastolic = obj.get("diastolic").and_then(|v| v.as_f64()).unwrap_or(0.0);
HealthStatValue::BloodPressure { systolic, diastolic }
} else {
HealthStatValue::Single(0.0)
}
}
_ => {
if let Some(num) = value.as_f64() {
HealthStatValue::Single(num)
} else if let Some(str_val) = value.as_str() {
HealthStatValue::String(str_val.to_string())
} else {
HealthStatValue::Single(0.0)
}
}
}
}
fn calculate_summary(stats: &[HealthStatistic]) -> TrendSummary {
let mut values: Vec<f64> = Vec::new();
for stat in stats {
match &stat.value {
HealthStatValue::Single(v) => values.push(*v),
HealthStatValue::BloodPressure { systolic, .. } => values.push(*systolic),
_ => {}
}
}
if values.is_empty() {
return TrendSummary {
latest: None,
earliest: None,
average: None,
min: None,
max: None,
trend: "stable".to_string(),
};
}
let latest = values.last().copied();
let earliest = values.first().copied();
let average = values.iter().sum::<f64>() / values.len() as f64;
let min = values.iter().fold(f64::INFINITY, |a, &b| a.min(b));
let max = values.iter().fold(f64::NEG_INFINITY, |a, &b| a.max(b));
let trend = if let (Some(l), Some(e)) = (latest, earliest) {
let change = ((l - e) / e * 100.0).abs();
if l > e && change > 5.0 {
"up"
} else if l < e && change > 5.0 {
"down"
} else {
"stable"
}
} else {
"stable"
};
TrendSummary {
latest,
earliest,
average: Some(average),
min: Some(min),
max: Some(max),
trend: trend.to_string(),
}
}

View file

@ -1,5 +1,6 @@
pub mod auth;
pub mod health;
pub mod health_stats;
pub mod permissions;
pub mod shares;
pub mod users;
@ -14,3 +15,4 @@ pub use permissions::check_permission;
pub use users::{get_profile, update_profile, delete_account, change_password, get_settings, update_settings};
pub use sessions::{get_sessions, revoke_session, revoke_all_sessions};
pub use medications::{create_medication, list_medications, get_medication, update_medication, delete_medication, log_dose, get_adherence};
pub use health_stats::{create_health_stat, list_health_stats, get_health_stat, update_health_stat, delete_health_stat, get_health_trends};

View file

@ -78,6 +78,7 @@ async fn main() -> anyhow::Result<()> {
// Get the underlying MongoDB database for security services
let database = db.get_database();
let mongo_client = database.client().clone();
// Initialize security services (Phase 2.6)
let audit_logger = security::AuditLogger::new(&database);
@ -92,6 +93,12 @@ async fn main() -> anyhow::Result<()> {
1440, // max_duration_minutes (24 hours)
);
// Initialize health stats repository (Phase 2.7) - using Collection pattern
let health_stats_collection = database.collection("health_statistics");
let health_stats_repo = models::health_stats::HealthStatisticsRepository::new(
health_stats_collection
);
// Create application state
let state = config::AppState {
db,
@ -100,8 +107,10 @@ async fn main() -> anyhow::Result<()> {
audit_logger: Some(audit_logger),
session_manager: Some(session_manager),
account_lockout: Some(account_lockout),
health_stats_repo: Some(health_stats_repo),
mongo_client: Some(mongo_client),
};
eprintln!("Building router with security middleware...");
// Build public routes (no auth required)
@ -146,6 +155,14 @@ async fn main() -> anyhow::Result<()> {
.route("/api/medications/:id/delete", post(handlers::delete_medication))
.route("/api/medications/:id/log", post(handlers::log_dose))
.route("/api/medications/:id/adherence", get(handlers::get_adherence))
// Health statistics management (Phase 2.7)
.route("/api/health-stats", post(handlers::create_health_stat))
.route("/api/health-stats", get(handlers::list_health_stats))
.route("/api/health-stats/trends", get(handlers::get_health_trends))
.route("/api/health-stats/:id", get(handlers::get_health_stat))
.route("/api/health-stats/:id", put(handlers::update_health_stat))
.route("/api/health-stats/:id", delete(handlers::delete_health_stat))
.layer(axum::middleware::from_fn_with_state(
state.clone(),
middleware::jwt_auth_middleware

View file

@ -0,0 +1,246 @@
use serde::{Deserialize, Serialize};
use mongodb::{bson::{oid::ObjectId, doc}, Collection, DateTime};
use futures::stream::TryStreamExt;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct HealthStatistic {
#[serde(rename = "_id", skip_serializing_if = "Option::is_none")]
pub id: Option<ObjectId>,
#[serde(rename = "healthStatId")]
pub health_stat_id: String,
#[serde(rename = "userId")]
pub user_id: String,
#[serde(rename = "profileId")]
pub profile_id: String,
#[serde(rename = "statType")]
pub stat_type: HealthStatType,
#[serde(rename = "value")]
pub value: HealthStatValue,
#[serde(rename = "unit")]
pub unit: String,
#[serde(rename = "recordedAt")]
pub recorded_at: DateTime,
#[serde(rename = "notes")]
pub notes: Option<String>,
#[serde(rename = "tags")]
pub tags: Vec<String>,
#[serde(rename = "createdAt")]
pub created_at: DateTime,
#[serde(rename = "updatedAt")]
pub updated_at: DateTime,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
pub enum HealthStatType {
Weight,
Height,
BloodPressure,
HeartRate,
Temperature,
BloodGlucose,
OxygenSaturation,
SleepHours,
Steps,
Calories,
Custom(String),
}
impl HealthStatType {
pub fn as_str(&self) -> &str {
match self {
HealthStatType::Weight => "weight",
HealthStatType::Height => "height",
HealthStatType::BloodPressure => "blood_pressure",
HealthStatType::HeartRate => "heart_rate",
HealthStatType::Temperature => "temperature",
HealthStatType::BloodGlucose => "blood_glucose",
HealthStatType::OxygenSaturation => "oxygen_saturation",
HealthStatType::SleepHours => "sleep_hours",
HealthStatType::Steps => "steps",
HealthStatType::Calories => "calories",
HealthStatType::Custom(name) => name,
}
}
pub fn default_unit(&self) -> &str {
match self {
HealthStatType::Weight => "kg",
HealthStatType::Height => "cm",
HealthStatType::BloodPressure => "mmHg",
HealthStatType::HeartRate => "bpm",
HealthStatType::Temperature => "°C",
HealthStatType::BloodGlucose => "mg/dL",
HealthStatType::OxygenSaturation => "%",
HealthStatType::SleepHours => "hours",
HealthStatType::Steps => "steps",
HealthStatType::Calories => "kcal",
HealthStatType::Custom(_) => "",
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(untagged)]
pub enum HealthStatValue {
Single(f64),
BloodPressure { systolic: f64, diastolic: f64 },
String(String),
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CreateHealthStatRequest {
pub profile_id: String,
#[serde(rename = "statType")]
pub stat_type: String,
pub value: serde_json::Value,
pub unit: Option<String>,
#[serde(rename = "recordedAt")]
pub recorded_at: Option<DateTime>,
pub notes: Option<String>,
pub tags: Option<Vec<String>>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct UpdateHealthStatRequest {
pub value: Option<serde_json::Value>,
pub unit: Option<String>,
#[serde(rename = "recordedAt")]
pub recorded_at: Option<DateTime>,
pub notes: Option<String>,
pub tags: Option<Vec<String>>,
}
#[derive(Clone)]
pub struct HealthStatisticsRepository {
pub collection: Collection<HealthStatistic>,
}
impl HealthStatisticsRepository {
pub fn new(collection: Collection<HealthStatistic>) -> Self {
Self { collection }
}
pub async fn create(&self, stat: HealthStatistic) -> Result<HealthStatistic, Box<dyn std::error::Error>> {
self.collection.insert_one(stat.clone(), None).await?;
Ok(stat)
}
pub async fn list_by_user(
&self,
user_id: &str,
stat_type: Option<&str>,
profile_id: Option<&str>,
limit: i64,
) -> Result<Vec<HealthStatistic>, Box<dyn std::error::Error>> {
let mut filter = doc! {
"userId": user_id
};
if let Some(stat_type) = stat_type {
filter.insert("statType", stat_type);
}
if let Some(profile_id) = profile_id {
filter.insert("profileId", profile_id);
}
let find_options = mongodb::options::FindOptions::builder()
.sort(doc! { "recordedAt": -1 })
.limit(limit)
.build();
let cursor = self.collection.find(filter, find_options).await?;
let results: Vec<_> = cursor.try_collect().await?;
Ok(results)
}
pub async fn get_by_id(&self, id: &ObjectId, user_id: &str) -> Result<Option<HealthStatistic>, Box<dyn std::error::Error>> {
let filter = doc! {
"_id": id,
"userId": user_id
};
let result = self.collection.find_one(filter, None).await?;
Ok(result)
}
pub async fn update(
&self,
id: &ObjectId,
user_id: &str,
update: UpdateHealthStatRequest,
) -> Result<Option<HealthStatistic>, Box<dyn std::error::Error>> {
let filter = doc! {
"_id": id,
"userId": user_id
};
let mut update_doc = doc! {};
if let Some(value) = update.value {
update_doc.insert("value", mongodb::bson::to_bson(&value)?);
}
if let Some(unit) = update.unit {
update_doc.insert("unit", unit);
}
if let Some(recorded_at) = update.recorded_at {
update_doc.insert("recordedAt", recorded_at);
}
if let Some(notes) = update.notes {
update_doc.insert("notes", notes);
}
if let Some(tags) = update.tags {
update_doc.insert("tags", tags);
}
update_doc.insert("updatedAt", DateTime::now());
let update = doc! {
"$set": update_doc
};
let result = self.collection.update_one(filter, update, None).await?;
if result.modified_count > 0 {
self.get_by_id(id, user_id).await
} else {
Ok(None)
}
}
pub async fn delete(&self, id: &ObjectId, user_id: &str) -> Result<bool, Box<dyn std::error::Error>> {
let filter = doc! {
"_id": id,
"userId": user_id
};
let result = self.collection.delete_one(filter, None).await?;
Ok(result.deleted_count > 0)
}
pub async fn get_trends(
&self,
user_id: &str,
profile_id: &str,
stat_type: &str,
days: i64,
) -> Result<Vec<HealthStatistic>, Box<dyn std::error::Error>> {
// Use chrono duration instead of DateTime arithmetic
let now = chrono::Utc::now();
let days_ago = now - chrono::Duration::days(days);
let days_ago_bson = DateTime::from_chrono(days_ago);
let filter = doc! {
"userId": user_id,
"profileId": profile_id,
"statType": stat_type,
"recordedAt": { "$gte": days_ago_bson }
};
let find_options = mongodb::options::FindOptions::builder()
.sort(doc! { "recordedAt": 1 })
.build();
let cursor = self.collection.find(filter, find_options).await?;
let results: Vec<_> = cursor.try_collect().await?;
Ok(results)
}
}

View file

@ -1,11 +1,11 @@
pub mod user;
pub mod audit_log;
pub mod family;
pub mod profile;
pub mod health_data;
pub mod health_stats;
pub mod lab_result;
pub mod medication;
pub mod appointment;
pub mod share;
pub mod permission;
pub mod profile;
pub mod refresh_token;
pub mod session;
pub mod audit_log;
pub mod share;
pub mod user;

99
backend/test-med-v2.sh Executable file
View file

@ -0,0 +1,99 @@
#!/bin/bash
BASE_URL="http://localhost:8001"
echo "=== Phase 2.7 Medication API Tests ==="
echo ""
# Test 1: Register
echo "Test 1: Register User"
RND=$((10000 + RANDOM % 90000))
REGISTER=$(curl -s -X POST -H "Content-Type: application/json" \
-d '{"email":"test'$RND'@example.com","username":"test'$RND'","password":"password123"}' \
"$BASE_URL/api/auth/register")
echo "$REGISTER"
TOKEN=$(echo "$REGISTER" | grep -o '"token":"[^"]*"' | cut -d'"' -f4 | head -1)
USER_ID=$(echo "$REGISTER" | grep -o '"user_id":"[^"]*"' | cut -d'"' -f4 | head -1)
echo "Token: ${TOKEN:0:50}..."
echo "User ID: $USER_ID"
echo ""
# Test 2: Create Medication
echo "Test 2: Create Medication"
CREATE=$(curl -s -w "\nStatus: %{http_code}" -X POST \
-H "Content-Type: application/json" \
-H "Authorization: Bearer $TOKEN" \
-d '{"profile_id":"'$USER_ID'","name":"Aspirin","dosage":"81mg","frequency":"daily"}' \
"$BASE_URL/api/medications")
echo "$CREATE"
MED_ID=$(echo "$CREATE" | grep -o '"id":"[^"]*"' | cut -d'"' -f4 | head -1)
echo "Med ID: $MED_ID"
echo ""
# Test 3: List Medications
echo "Test 3: List Medications"
curl -s -w "\nStatus: %{http_code}\n" \
-H "Authorization: Bearer $TOKEN" \
"$BASE_URL/api/medications"
echo ""
# Test 4: Get Specific Medication
if [ -n "$MED_ID" ]; then
echo "Test 4: Get Medication $MED_ID"
curl -s -w "\nStatus: %{http_code}\n" \
-H "Authorization: Bearer $TOKEN" \
"$BASE_URL/api/medications/$MED_ID"
echo ""
fi
# Test 5: Update Medication
if [ -n "$MED_ID" ]; then
echo "Test 5: Update Medication"
curl -s -w "\nStatus: %{http_code}\n" -X POST \
-H "Content-Type: application/json" \
-H "Authorization: Bearer $TOKEN" \
-d '{"name":"Aspirin Updated"}' \
"$BASE_URL/api/medications/$MED_ID"
echo ""
fi
# Test 6: Log Dose
if [ -n "$MED_ID" ]; then
echo "Test 6: Log Dose"
curl -s -w "\nStatus: %{http_code}\n" -X POST \
-H "Content-Type: application/json" \
-H "Authorization: Bearer $TOKEN" \
-d '{"taken":true,"notes":"Taken with food"}' \
"$BASE_URL/api/medications/$MED_ID/log"
echo ""
fi
# Test 7: Get Adherence
if [ -n "$MED_ID" ]; then
echo "Test 7: Get Adherence"
curl -s -w "\nStatus: %{http_code}\n" \
-H "Authorization: Bearer $TOKEN" \
"$BASE_URL/api/medications/$MED_ID/adherence"
echo ""
fi
# Test 8: Unauthorized Access
echo "Test 8: Unauthorized Access (should be 401)"
curl -s -w "\nStatus: %{http_code}\n" \
"$BASE_URL/api/medications"
echo ""
# Test 9: Get User Profile
echo "Test 9: Get User Profile"
curl -s -w "\nStatus: %{http_code}\n" \
-H "Authorization: Bearer $TOKEN" \
"$BASE_URL/api/users/me"
echo ""
# Test 10: List Shares
echo "Test 10: List Shares"
curl -s -w "\nStatus: %{http_code}\n" \
-H "Authorization: Bearer $TOKEN" \
"$BASE_URL/api/shares"
echo ""
echo "=== Tests Complete ==="

View file

@ -0,0 +1,142 @@
#!/bin/bash
# Normogen Medication Management API Test Suite
API_URL="http://solaria.solivarez.com.ar:8001"
TEST_USER="med-test-$(date +%s)@example.com"
TEST_USERNAME="medtest$(date +%s)"
TEST_PASSWORD="SecurePass123!"
echo "=========================================="
echo "Medication Management API Test Suite"
echo "=========================================="
echo ""
# Test 1: Health Check
echo "Test 1: Health Check"
HEALTH=$(curl -s "$API_URL/health")
if echo "$HEALTH" | grep -q "ok"; then
echo "✓ Health check passed"
else
echo "✗ Health check failed"
exit 1
fi
echo ""
# Test 2: Register User
echo "Test 2: Register New User"
REGISTER=$(curl -s -X POST "$API_URL/api/auth/register" -H "Content-Type: application/json" -d '{"email":"'$TEST_USER'","username":"'$TEST_USERNAME'","password":"'$TEST_PASSWORD'","first_name":"Test","last_name":"User"}')
if echo "$REGISTER" | grep -q "access_token"; then
echo "✓ User registration successful"
ACCESS_TOKEN=$(echo "$REGISTER" | grep -o '"access_token":"[^"]*' | cut -d'"' -f4)
else
echo "✗ User registration failed"
exit 1
fi
echo ""
# Test 3: Login
echo "Test 3: Login"
LOGIN=$(curl -s -X POST "$API_URL/api/auth/login" -H "Content-Type: application/json" -d '{"email":"'$TEST_USER'","password":"'$TEST_PASSWORD'"}')
if echo "$LOGIN" | grep -q "access_token"; then
echo "✓ Login successful"
ACCESS_TOKEN=$(echo "$LOGIN" | grep -o '"access_token":"[^"]*' | cut -d'"' -f4)
else
echo "✗ Login failed"
exit 1
fi
echo ""
# Test 4: Create Medication
echo "Test 4: Create Medication"
CREATE=$(curl -s -X POST "$API_URL/api/medications" -H "Content-Type: application/json" -H "Authorization: Bearer $ACCESS_TOKEN" -d '{"medication_name":"Lisinopril","dosage":"10mg","frequency":"once_daily","instructions":"Take with breakfast","start_date":"2026-03-05"}')
if echo "$CREATE" | grep -q "medication_name"; then
echo "✓ Medication created successfully"
MEDICATION_ID=$(echo "$CREATE" | grep -o '"id":"[^"]*' | head -1 | cut -d'"' -f3)
echo "Medication ID: $MEDICATION_ID"
else
echo "✗ Medication creation failed"
echo "Response: $CREATE"
exit 1
fi
echo ""
# Test 5: List All Medications
echo "Test 5: List All Medications"
LIST=$(curl -s -X GET "$API_URL/api/medications" -H "Authorization: Bearer $ACCESS_TOKEN")
if echo "$LIST" | grep -q "Lisinopril"; then
echo "✓ List medications successful"
else
echo "✗ List medications failed"
echo "Response: $LIST"
exit 1
fi
echo ""
# Test 6: Get Specific Medication
echo "Test 6: Get Medication Details"
if [ ! -z "$MEDICATION_ID" ]; then
GET=$(curl -s -X GET "$API_URL/api/medications/$MEDICATION_ID" -H "Authorization: Bearer $ACCESS_TOKEN")
if echo "$GET" | grep -q "Lisinopril"; then
echo "✓ Get medication successful"
else
echo "✗ Get medication failed"
fi
fi
echo ""
# Test 7: Update Medication
echo "Test 7: Update Medication"
if [ ! -z "$MEDICATION_ID" ]; then
UPDATE=$(curl -s -X PUT "$API_URL/api/medications/$MEDICATION_ID" -H "Content-Type: application/json" -H "Authorization: Bearer $ACCESS_TOKEN" -d '{"medication_name":"Lisinopril","dosage":"20mg","frequency":"once_daily","instructions":"Take with breakfast","start_date":"2026-03-05"}')
if echo "$UPDATE" | grep -q "20mg"; then
echo "✓ Update medication successful"
else
echo "✗ Update medication failed"
fi
fi
echo ""
# Test 8: Log Dose - Taken
echo "Test 8: Log Dose (Taken)"
if [ ! -z "$MEDICATION_ID" ]; then
LOG1=$(curl -s -X POST "$API_URL/api/medications/$MEDICATION_ID/log" -H "Content-Type: application/json" -H "Authorization: Bearer $ACCESS_TOKEN" -d '{"status":"taken","notes":"Taken with breakfast"}')
if echo "$LOG1" | grep -q "logged_at"; then
echo "✓ Dose logged successfully (taken)"
else
echo "✗ Log dose failed"
fi
fi
echo ""
# Test 9: Calculate Adherence
echo "Test 9: Calculate Adherence"
if [ ! -z "$MEDICATION_ID" ]; then
ADHERENCE=$(curl -s -X GET "$API_URL/api/medications/$MEDICATION_ID/adherence" -H "Authorization: Bearer $ACCESS_TOKEN")
echo "✓ Adherence calculated"
echo "Response: $ADHERENCE"
fi
echo ""
# Test 10: Unauthorized Access Test
echo "Test 10: Unauthorized Access (should fail)"
UNAUTH=$(curl -s -X GET "$API_URL/api/medications")
if echo "$UNAUTH" | grep -q "401\|Unauthorized"; then
echo "✓ Unauthorized access properly blocked"
else
echo "✗ Security issue - unauthorized access not blocked"
fi
echo ""
# Test 11: Delete Medication
echo "Test 11: Delete Medication"
if [ ! -z "$MEDICATION_ID" ]; then
DELETE=$(curl -s -X DELETE "$API_URL/api/medications/$MEDICATION_ID" -H "Authorization: Bearer $ACCESS_TOKEN")
echo "✓ Delete medication successful"
fi
echo ""
echo "=========================================="
echo "Test Suite Complete"
echo "=========================================="

241
backend/test-solaria-v2.sh Normal file
View file

@ -0,0 +1,241 @@
#!/bin/bash
echo "=========================================="
echo "Phase 2.7 MVP - Comprehensive API Test"
echo "Running on Solaria server"
echo "=========================================="
echo ""
BASE_URL="http://localhost:8001"
MED_ID=""
RANDOM_NUM=0
# Test 1: Health Check
echo "🔍 Test 1: Health Check"
echo "Endpoint: GET /health"
RESPONSE=$(curl -s -w "\nHTTP Status: %{http_code}\n" "$BASE_URL/health")
echo "$RESPONSE"
if echo "$RESPONSE" | grep -q "HTTP Status: 200"; then
echo "✅ PASS"
else
echo "❌ FAIL"
fi
echo ""
# Test 2: Register New User
echo "🔍 Test 2: Register New User"
echo "Endpoint: POST /api/auth/register"
RANDOM_NUM=$((10000 + RANDOM % 90000))
REGISTER_RESPONSE=$(curl -s -w "\nHTTP Status: %{http_code}\n" -X POST \
-H "Content-Type: application/json" \
-d '{
"email": "med-test-'$RANDOM_NUM'@example.com",
"username": "medtest'$RANDOM_NUM'",
"password": "password123",
"recovery_phrase": "recovery phrase"
}' \
"$BASE_URL/api/auth/register")
echo "$REGISTER_RESPONSE"
TOKEN=$(echo "$REGISTER_RESPONSE" | grep -o '"token":"[^"]*"' | cut -d'"' -f4)
USER_ID=$(echo "$REGISTER_RESPONSE" | grep -o '"user_id":"[^"]*"' | cut -d'"' -f4)
if echo "$REGISTER_RESPONSE" | grep -q "HTTP Status: 201"; then
echo "✅ PASS"
echo "User ID: $USER_ID"
else
echo "❌ FAIL"
fi
echo ""
# Test 3: Login with same credentials
echo "🔍 Test 3: Login"
echo "Endpoint: POST /api/auth/login"
LOGIN_RESPONSE=$(curl -s -w "\nHTTP Status: %{http_code}\n" -X POST \
-H "Content-Type: application/json" \
-d '{
"email": "med-test-'$RANDOM_NUM'@example.com",
"password": "password123"
}' \
"$BASE_URL/api/auth/login")
echo "$LOGIN_RESPONSE"
TOKEN=$(echo "$LOGIN_RESPONSE" | grep -o '"token":"[^"]*"' | cut -d'"' -f4)
if [ -n "$TOKEN" ]; then
echo "✅ PASS"
echo "Token obtained: ${TOKEN:0:50}..."
else
echo "❌ FAIL"
fi
echo ""
# Test 4: Create Medication
echo "🔍 Test 4: Create Medication"
echo "Endpoint: POST /api/medications"
CREATE_MED_RESPONSE=$(curl -s -w "\nHTTP Status: %{http_code}\n" -X POST \
-H "Content-Type: application/json" \
-H "Authorization: Bearer $TOKEN" \
-d '{
"profile_id": "'$USER_ID'",
"name": "Aspirin",
"dosage": "81mg",
"frequency": "Once daily",
"instructions": "Take with food",
"start_date": "2024-01-01",
"reminders": [{"time": "08:00", "days": ["mon", "tue", "wed", "thu", "fri", "sat", "sun"]}]
}' \
"$BASE_URL/api/medications")
echo "$CREATE_MED_RESPONSE"
MED_ID=$(echo "$CREATE_MED_RESPONSE" | grep -o '"id":"[^"]*"' | head -1 | cut -d'"' -f4)
if echo "$CREATE_MED_RESPONSE" | grep -q "HTTP Status: 201\|HTTP Status: 200"; then
echo "✅ PASS"
echo "Medication ID: $MED_ID"
else
echo "❌ FAIL"
fi
echo ""
# Test 5: List Medications
echo "🔍 Test 5: List Medications"
echo "Endpoint: GET /api/medications"
LIST_MEDS_RESPONSE=$(curl -s -w "\nHTTP Status: %{http_code}\n" \
-H "Authorization: Bearer $TOKEN" \
"$BASE_URL/api/medications")
echo "$LIST_MEDS_RESPONSE"
if echo "$LIST_MEDS_RESPONSE" | grep -q "HTTP Status: 200"; then
echo "✅ PASS"
else
echo "❌ FAIL"
fi
echo ""
# Test 6: Get Specific Medication
if [ -n "$MED_ID" ]; then
echo "🔍 Test 6: Get Specific Medication"
echo "Endpoint: GET /api/medications/$MED_ID"
GET_MED_RESPONSE=$(curl -s -w "\nHTTP Status: %{http_code}\n" \
-H "Authorization: Bearer $TOKEN" \
"$BASE_URL/api/medications/$MED_ID")
echo "$GET_MED_RESPONSE"
if echo "$GET_MED_RESPONSE" | grep -q "HTTP Status: 200"; then
echo "✅ PASS"
else
echo "❌ FAIL"
fi
echo ""
fi
# Test 7: Update Medication
if [ -n "$MED_ID" ]; then
echo "🔍 Test 7: Update Medication"
echo "Endpoint: POST /api/medications/$MED_ID"
UPDATE_MED_RESPONSE=$(curl -s -w "\nHTTP Status: %{http_code}\n" -X POST \
-H "Content-Type: application/json" \
-H "Authorization: Bearer $TOKEN" \
-d '{
"name": "Aspirin Updated",
"dosage": "100mg"
}' \
"$BASE_URL/api/medications/$MED_ID")
echo "$UPDATE_MED_RESPONSE"
if echo "$UPDATE_MED_RESPONSE" | grep -q "HTTP Status: 200"; then
echo "✅ PASS"
else
echo "❌ FAIL"
fi
echo ""
fi
# Test 8: Log Dose
if [ -n "$MED_ID" ]; then
echo "🔍 Test 8: Log Dose"
echo "Endpoint: POST /api/medications/$MED_ID/log"
LOG_DOSE_RESPONSE=$(curl -s -w "\nHTTP Status: %{http_code}\n" -X POST \
-H "Content-Type: application/json" \
-H "Authorization: Bearer $TOKEN" \
-d '{
"taken": true,
"scheduled_time": "2024-01-01T08:00:00Z",
"notes": "Taken with breakfast"
}' \
"$BASE_URL/api/medications/$MED_ID/log")
echo "$LOG_DOSE_RESPONSE"
if echo "$LOG_DOSE_RESPONSE" | grep -q "HTTP Status: 201\|HTTP Status: 200"; then
echo "✅ PASS"
else
echo "❌ FAIL"
fi
echo ""
fi
# Test 9: Get Adherence
if [ -n "$MED_ID" ]; then
echo "🔍 Test 9: Get Adherence"
echo "Endpoint: GET /api/medications/$MED_ID/adherence"
ADHERENCE_RESPONSE=$(curl -s -w "\nHTTP Status: %{http_code}\n" \
-H "Authorization: Bearer $TOKEN" \
"$BASE_URL/api/medications/$MED_ID/adherence")
echo "$ADHERENCE_RESPONSE"
if echo "$ADHERENCE_RESPONSE" | grep -q "HTTP Status: 200"; then
echo "✅ PASS"
else
echo "❌ FAIL"
fi
echo ""
fi
# Test 10: Unauthorized Access (No Token)
echo "🔍 Test 10: Unauthorized Access (No Token)"
echo "Endpoint: GET /api/medications"
UNAUTH_RESPONSE=$(curl -s -w "\nHTTP Status: %{http_code}\n" \
"$BASE_URL/api/medications")
echo "$UNAUTH_RESPONSE"
if echo "$UNAUTH_RESPONSE" | grep -q "HTTP Status: 401"; then
echo "✅ PASS - Correctly blocked unauthorized access"
else
echo "❌ FAIL - Should return 401"
fi
echo ""
# Test 11: Get User Profile
echo "🔍 Test 11: Get User Profile"
echo "Endpoint: GET /api/users/me"
PROFILE_RESPONSE=$(curl -s -w "\nHTTP Status: %{http_code}\n" \
-H "Authorization: Bearer $TOKEN" \
"$BASE_URL/api/users/me")
echo "$PROFILE_RESPONSE"
if echo "$PROFILE_RESPONSE" | grep -q "HTTP Status: 200"; then
echo "✅ PASS"
else
echo "❌ FAIL"
fi
echo ""
# Test 12: List Shares
echo "🔍 Test 12: List Shares"
echo "Endpoint: GET /api/shares"
SHARES_RESPONSE=$(curl -s -w "\nHTTP Status: %{http_code}\n" \
-H "Authorization: Bearer $TOKEN" \
"$BASE_URL/api/shares")
echo "$SHARES_RESPONSE"
if echo "$SHARES_RESPONSE" | grep -q "HTTP Status: 200"; then
echo "✅ PASS"
else
echo "❌ FAIL"
fi
echo ""
# Test 13: Get Sessions
echo "🔍 Test 13: Get Sessions"
echo "Endpoint: GET /api/sessions"
SESSIONS_RESPONSE=$(curl -s -w "\nHTTP Status: %{http_code}\n" \
-H "Authorization: Bearer $TOKEN" \
"$BASE_URL/api/sessions")
echo "$SESSIONS_RESPONSE"
if echo "$SESSIONS_RESPONSE" | grep -q "HTTP Status: 200"; then
echo "✅ PASS"
else
echo "❌ FAIL"
fi
echo ""
echo "=========================================="
echo "All Tests Complete!"
echo "=========================================="