Skip to content

Commit 8a709d8

Browse files
authored
Merge pull request #426 from archanavdev/fix/four-issues
Fix 4 issues: premium overflow (#382), trigger length check (#379), policy cache invalidation (#365), webhook per-user limit (#372)
2 parents 2b09b37 + e53759c commit 8a709d8

7 files changed

Lines changed: 204 additions & 4 deletions

File tree

backend/src/config.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,7 @@ class Settings(BaseSettings):
6161
webhook_secret_key: str = "webhook-secret-key-change-in-production"
6262
webhook_max_retries: int = 3
6363
webhook_delivery_timeout: int = 30
64+
webhook_max_per_user: int = 10
6465

6566
# Logging
6667
log_level: str = "INFO"

backend/src/routes/claims.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
)
2323
from ..services.storage_service import storage_service
2424
from ..services.webhook_service import dispatch_webhook_event
25+
from ..cache import invalidate_policy_cache
2526

2627
router = APIRouter(prefix="/claims", tags=["claims"])
2728
logger = logging.getLogger(__name__)
@@ -102,6 +103,8 @@ async def create_claim(
102103
db.commit()
103104
db.refresh(claim)
104105

106+
invalidate_policy_cache(current_user.id)
107+
105108
background_tasks.add_task(
106109
dispatch_webhook_event,
107110
db=db,
@@ -174,6 +177,8 @@ async def create_claim_with_file(
174177
db.commit()
175178
db.refresh(claim)
176179

180+
invalidate_policy_cache(current_user.id)
181+
177182
background_tasks.add_task(
178183
dispatch_webhook_event,
179184
db=db,

backend/src/routes/webhooks.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
from fastapi import APIRouter, Depends, status, Query
77
from sqlalchemy.orm import Session
88

9+
from ..config import get_settings
910
from ..database import get_db
1011
from ..dependencies import get_current_active_user
1112
from ..errors import StellarInsureError
@@ -32,6 +33,11 @@ def __init__(self, detail: str = "An active webhook with this URL already exists
3233
super().__init__(status.HTTP_409_CONFLICT, detail, "WEBHOOK_002")
3334

3435

36+
class WebhookLimitExceededError(StellarInsureError):
37+
def __init__(self, detail: str = "Webhook limit reached for this user"):
38+
super().__init__(status.HTTP_429_TOO_MANY_REQUESTS, detail, "WEBHOOK_003")
39+
40+
3541
def _format_webhook(webhook: Webhook) -> WebhookResponse:
3642
return WebhookResponse(
3743
id=webhook.id,
@@ -60,6 +66,15 @@ async def create_webhook(
6066
current_user: User = Depends(get_current_active_user),
6167
db: Session = Depends(get_db),
6268
):
69+
settings = get_settings()
70+
webhook_count = (
71+
db.query(Webhook)
72+
.filter(Webhook.user_id == current_user.id)
73+
.count()
74+
)
75+
if webhook_count >= settings.webhook_max_per_user:
76+
raise WebhookLimitExceededError()
77+
6378
existing = (
6479
db.query(Webhook)
6580
.filter(Webhook.user_id == current_user.id, Webhook.url == body.url, Webhook.is_active == True)

backend/tests/test_cache.py

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -150,3 +150,73 @@ def test_create_policy_invalidates_cache(self, client, auth_user, auth_headers):
150150
list_response = client.get("/policies/", headers=auth_headers)
151151
assert list_response.status_code == 200
152152
assert list_response.json()["total"] >= 1
153+
154+
155+
class TestClaimCacheInvalidation:
156+
"""Test that claim submission invalidates policy cache."""
157+
158+
def test_create_claim_text_invalidates_policy_cache(
159+
self, client, auth_user, auth_headers, policy_factory
160+
):
161+
"""Text claim creation should trigger policy cache invalidation."""
162+
policy = policy_factory(auth_user, coverage_amount=1000.0)
163+
164+
import src.routes.claims as claims_module
165+
from unittest.mock import patch
166+
167+
with patch.object(claims_module, "invalidate_policy_cache") as mock_invalidate:
168+
response = client.post(
169+
"/claims/",
170+
headers=auth_headers,
171+
json={
172+
"policy_id": policy.id,
173+
"claim_amount": 300.0,
174+
"proof": "Satellite weather evidence",
175+
},
176+
)
177+
178+
assert response.status_code == 201
179+
mock_invalidate.assert_called_once_with(auth_user.id)
180+
181+
def test_create_claim_file_upload_invalidates_policy_cache(
182+
self, client, auth_user, auth_headers, policy_factory
183+
):
184+
"""File upload claim creation should trigger policy cache invalidation."""
185+
from io import BytesIO
186+
policy = policy_factory(auth_user, coverage_amount=1000.0)
187+
188+
import src.routes.claims as claims_module
189+
from unittest.mock import patch
190+
191+
with patch.object(claims_module, "invalidate_policy_cache") as mock_invalidate:
192+
response = client.post(
193+
"/claims/upload",
194+
headers=auth_headers,
195+
data={"policy_id": str(policy.id), "claim_amount": "275.0"},
196+
files={"file": ("proof.png", BytesIO(b"png"), "image/png")},
197+
)
198+
199+
assert response.status_code == 201
200+
mock_invalidate.assert_called_once_with(auth_user.id)
201+
202+
def test_create_claim_failure_does_not_invalidate_cache(
203+
self, client, auth_user, auth_headers
204+
):
205+
"""Failed claim creation should NOT trigger policy cache invalidation."""
206+
import src.routes.claims as claims_module
207+
from unittest.mock import patch
208+
209+
with patch.object(claims_module, "invalidate_policy_cache") as mock_invalidate:
210+
# Non-existent policy
211+
response = client.post(
212+
"/claims/",
213+
headers=auth_headers,
214+
json={
215+
"policy_id": 99999,
216+
"claim_amount": 300.0,
217+
"proof": "Satellite weather evidence",
218+
},
219+
)
220+
221+
assert response.status_code == 404
222+
mock_invalidate.assert_not_called()

smartcontract/src/error.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,4 +46,6 @@ pub enum Error {
4646
OracleConditionNotMet = 32,
4747
ProofTooLong = 33,
4848
ClaimAmountOverflow = 34,
49+
PremiumOverflow = 35,
50+
TriggerConditionTooLong = 36,
4951
}

smartcontract/src/lib.rs

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ pub use types::*;
4949
pub struct StellarInsure;
5050

5151
const MAX_POLICIES: u64 = 1_000_000;
52+
const MAX_TRIGGER_CONDITION_LEN: u32 = 256;
5253

5354
#[contractimpl]
5455
impl StellarInsure {
@@ -142,6 +143,10 @@ impl StellarInsure {
142143
return Err(Error::InvalidDuration);
143144
}
144145

146+
if trigger_condition.len() > MAX_TRIGGER_CONDITION_LEN {
147+
return Err(Error::TriggerConditionTooLong);
148+
}
149+
145150
let policy_id = storage::get_policy_counter(&env);
146151
let max_policies = storage::get_max_policies(&env);
147152
if policy_id >= max_policies {
@@ -228,7 +233,7 @@ impl StellarInsure {
228233
}
229234

230235
let total_premium = storage::get_total_premium(&env);
231-
storage::set_total_premium(&env, total_premium + amount);
236+
storage::set_total_premium(&env, total_premium.checked_add(amount).ok_or(Error::PremiumOverflow)?);
232237

233238
events::publish_premium_paid(
234239
&env,
@@ -659,7 +664,7 @@ impl StellarInsure {
659664
);
660665

661666
let total_premium = storage::get_total_premium(&env);
662-
storage::set_total_premium(&env, total_premium + additional_premium);
667+
storage::set_total_premium(&env, total_premium.checked_add(additional_premium).ok_or(Error::PremiumOverflow)?);
663668

664669
let old_coverage = policy.coverage_amount;
665670
policy.coverage_amount = new_coverage;
@@ -720,7 +725,7 @@ impl StellarInsure {
720725
);
721726

722727
let total_premium = storage::get_total_premium(&env);
723-
storage::set_total_premium(&env, total_premium + additional_premium);
728+
storage::set_total_premium(&env, total_premium.checked_add(additional_premium).ok_or(Error::PremiumOverflow)?);
724729

725730
let old_end_time = policy.end_time;
726731
policy.end_time = old_end_time + extra_seconds;
@@ -1128,7 +1133,7 @@ impl StellarInsure {
11281133
);
11291134

11301135
let total_premium = storage::get_total_premium(&env);
1131-
storage::set_total_premium(&env, total_premium + renewal_premium);
1136+
storage::set_total_premium(&env, total_premium.checked_add(renewal_premium).ok_or(Error::PremiumOverflow)?);
11321137

11331138
policy.premium = renewal_premium;
11341139
policy.end_time = new_end_time;

smartcontract/src/test.rs

Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1578,6 +1578,108 @@ fn test_partial_claim_exceeding_remaining_coverage_is_rejected() {
15781578
client.submit_claim(&policy_id, &100_000, &String::from_str(&env, "proof2"));
15791579
}
15801580

1581+
// ── Issue #379 — Trigger condition length check ───────────────────────────────
1582+
1583+
#[test]
1584+
fn test_create_policy_accepts_max_length_trigger_condition() {
1585+
let (env, contract_id, _admin, policyholder, _token) = setup_insurance_contract();
1586+
let client = StellarInsureClient::new(&env, &contract_id);
1587+
1588+
let sac = soroban_sdk::token::StellarAssetClient::new(&env, &_token);
1589+
sac.mint(&policyholder, &1_000_000);
1590+
1591+
// Exactly 256 characters (the limit)
1592+
let long_condition = "X".repeat(256);
1593+
let premium = client.calculate_premium(&PolicyType::Weather, &1_000_000, &2_592_000);
1594+
let policy_id = client.create_policy(
1595+
&policyholder,
1596+
&PolicyType::Weather,
1597+
&1_000_000,
1598+
&premium,
1599+
&2_592_000,
1600+
&String::from_str(&env, &long_condition),
1601+
);
1602+
assert_eq!(policy_id, 0);
1603+
}
1604+
1605+
#[test]
1606+
#[should_panic(expected = "#36")]
1607+
fn test_create_policy_rejects_oversized_trigger_condition() {
1608+
let (env, contract_id, _admin, policyholder, _token) = setup_insurance_contract();
1609+
let client = StellarInsureClient::new(&env, &contract_id);
1610+
1611+
let sac = soroban_sdk::token::StellarAssetClient::new(&env, &_token);
1612+
sac.mint(&policyholder, &1_000_000);
1613+
1614+
// 257 characters — exceeds the limit
1615+
let long_condition = "X".repeat(257);
1616+
let premium = client.calculate_premium(&PolicyType::Weather, &1_000_000, &2_592_000);
1617+
client.create_policy(
1618+
&policyholder,
1619+
&PolicyType::Weather,
1620+
&1_000_000,
1621+
&premium,
1622+
&2_592_000,
1623+
&String::from_str(&env, &long_condition),
1624+
);
1625+
}
1626+
1627+
// ── Issue #382 — Checked addition for total premium ────────────────────────────
1628+
1629+
#[test]
1630+
fn test_pay_premium_updates_total_premium() {
1631+
let (env, contract_id, _admin, policyholder, _token) = setup_insurance_contract();
1632+
let client = StellarInsureClient::new(&env, &contract_id);
1633+
1634+
let premium = client.calculate_premium(&PolicyType::Weather, &1_000_000, &2_592_000);
1635+
// Mint enough tokens for premium
1636+
let sac = soroban_sdk::token::StellarAssetClient::new(&env, &_token);
1637+
sac.mint(&policyholder, &premium);
1638+
1639+
let policy_id = client.create_policy(
1640+
&policyholder,
1641+
&PolicyType::Weather,
1642+
&1_000_000,
1643+
&premium,
1644+
&2_592_000,
1645+
&String::from_str(&env, "temperature < 0"),
1646+
);
1647+
client.pay_premium(&policy_id, &premium);
1648+
1649+
let stats = client.get_treasury_stats();
1650+
assert_eq!(stats.total_premium_collected, premium);
1651+
}
1652+
1653+
#[test]
1654+
fn test_pay_premium_updates_total_premium_correctly() {
1655+
let (env, contract_id, _admin, policyholder, _token) = setup_insurance_contract();
1656+
let client = StellarInsureClient::new(&env, &contract_id);
1657+
1658+
let premium = client.calculate_premium(&PolicyType::Weather, &1_000_000, &2_592_000);
1659+
let two_premiums = premium.checked_mul(2).unwrap();
1660+
let sac = soroban_sdk::token::StellarAssetClient::new(&env, &_token);
1661+
sac.mint(&policyholder, &two_premiums);
1662+
1663+
let policy_id = client.create_policy(
1664+
&policyholder,
1665+
&PolicyType::Weather,
1666+
&1_000_000,
1667+
&premium,
1668+
&2_592_000,
1669+
&String::from_str(&env, "temperature < 0"),
1670+
);
1671+
client.pay_premium(&policy_id, &premium);
1672+
1673+
let stats = client.get_treasury_stats();
1674+
assert_eq!(stats.total_premium_collected, premium);
1675+
1676+
// A second premium payment on the same policy also increases the total
1677+
client.pay_premium(&policy_id, &premium);
1678+
1679+
let stats = client.get_treasury_stats();
1680+
assert_eq!(stats.total_premium_collected, two_premiums);
1681+
}
1682+
15811683
// ── Issue #381 — Checked arithmetic ───────────────────────────────────────────
15821684

15831685
#[test]

0 commit comments

Comments
 (0)