Skip to content

Commit 2d7f928

Browse files
authored
Merge pull request #425 from aadviksinghdebug/fix/storage-errors-i18n-and-trigger-draft
Fix/storage errors i18n and trigger draft
2 parents 71211f9 + ac4fe2a commit 2d7f928

10 files changed

Lines changed: 363 additions & 108 deletions

File tree

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,8 +50,11 @@ __pycache__/
5050
*.so
5151
.Python
5252
venv/
53+
.venv/
5354
env/
5455
ENV/
56+
.coverage
57+
coverage.xml
5558

5659
# Docker
5760
.dockerignore

backend/src/errors.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,7 @@ def __init__(self, detail: str = "Invalid or expired storage token"):
7575

7676
class FileTooLargeError(StellarInsureError):
7777
def __init__(self, detail: str = "File size exceeds limit"):
78-
super().__init__(status.HTTP_400_BAD_REQUEST, detail, "STORAGE_003")
78+
super().__init__(status.HTTP_413_CONTENT_TOO_LARGE, detail, "STORAGE_003")
7979

8080
class InvalidFileTypeError(StellarInsureError):
8181
def __init__(self, detail: str = "File type not allowed"):

backend/src/services/storage_service.py

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
from typing import Optional, List
99
from fastapi import UploadFile, HTTPException, status
1010
from ..config import get_settings
11+
from ..errors import InvalidFileTypeError, FileTooLargeError
1112

1213
settings = get_settings()
1314
logger = logging.getLogger(__name__)
@@ -28,15 +29,13 @@ def validate_file(self, file: UploadFile):
2829
# Validate extension
2930
ext = os.path.splitext(file.filename)[1].lower() if file.filename else ""
3031
if ext not in self.allowed_extensions:
31-
raise HTTPException(
32-
status_code=status.HTTP_400_BAD_REQUEST,
32+
raise InvalidFileTypeError(
3333
detail=f"File extension {ext} not allowed. Allowed: {', '.join(self.allowed_extensions)}"
3434
)
3535

3636
# Validate content type
3737
if file.content_type not in self.allowed_content_types:
38-
raise HTTPException(
39-
status_code=status.HTTP_400_BAD_REQUEST,
38+
raise InvalidFileTypeError(
4039
detail=f"Content type {file.content_type} not allowed. Allowed: {', '.join(self.allowed_content_types)}"
4140
)
4241

@@ -46,8 +45,7 @@ async def upload_file(self, file: UploadFile, folder: str = "general") -> str:
4645
# Read content to check size
4746
content = await file.read()
4847
if len(content) > self.max_size:
49-
raise HTTPException(
50-
status_code=status.HTTP_413_REQUEST_ENTITY_TOO_LARGE,
48+
raise FileTooLargeError(
5149
detail=f"File size exceeds limit of {self.max_size / (1024 * 1024):.1f}MB"
5250
)
5351

backend/tests/test_storage.py

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,8 @@
55
from io import BytesIO
66
from fastapi import UploadFile, HTTPException
77
from fastapi.testclient import TestClient
8+
from src.errors import InvalidFileTypeError, FileTooLargeError
89
from src.services.storage_service import StorageService
9-
from src.main import app
1010

1111
class TestStorageService:
1212
@pytest.fixture
@@ -45,18 +45,20 @@ async def test_upload_invalid_type(self, storage_service):
4545
content = b"test content"
4646
file = UploadFile(filename="test.exe", file=BytesIO(content), headers={"content-type": "application/x-msdownload"})
4747

48-
with pytest.raises(HTTPException) as exc:
48+
with pytest.raises(InvalidFileTypeError) as exc:
4949
await storage_service.upload_file(file)
5050
assert exc.value.status_code == 400
51+
assert exc.value.error_code == "STORAGE_004"
5152

5253
@pytest.mark.asyncio
5354
async def test_upload_too_large(self, storage_service):
5455
content = b"a" * 2000 # 2KB > 1KB limit
5556
file = UploadFile(filename="large.png", file=BytesIO(content), headers={"content-type": "image/png"})
5657

57-
with pytest.raises(HTTPException) as exc:
58+
with pytest.raises(FileTooLargeError) as exc:
5859
await storage_service.upload_file(file)
5960
assert exc.value.status_code == 413
61+
assert exc.value.error_code == "STORAGE_003"
6062

6163
def test_secure_url_generation_and_validation(self, storage_service):
6264
file_path = "test/file.png"
@@ -100,6 +102,7 @@ def test_invalid_token_signature(self, storage_service):
100102

101103
class TestStorageIntegration:
102104
def test_storage_route_access(self):
105+
from src.main import app
103106
client = TestClient(app)
104107
# Unauthorized access or invalid token should return 403
105108
response = client.get("/storage/files/invalid-token")

frontend/src/app/create/create-page-client.test.tsx

Lines changed: 36 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,9 +18,20 @@ vi.mock("@stellar/freighter-api", () => ({
1818
}));
1919

2020
vi.mock("@/components/trigger-condition-builder", () => ({
21-
TriggerConditionBuilder: ({ onChange }: { onChange: (value: string) => void }) => (
21+
TriggerConditionBuilder: ({
22+
initialRules,
23+
onChange,
24+
}: {
25+
initialRules?: { field: string; operator: string; value: string }[];
26+
onChange: (value: string) => void;
27+
}) => (
2228
<input
2329
aria-label="Trigger mock input"
30+
defaultValue={
31+
initialRules
32+
? initialRules.map((r) => `${r.field} ${r.operator} ${r.value}`).join(" AND ")
33+
: ""
34+
}
2435
onChange={(event) => onChange(event.target.value)}
2536
placeholder="Trigger mock input"
2637
type="text"
@@ -114,4 +125,28 @@ describe("CreatePolicyPageClient", () => {
114125

115126
expect(screen.getByText(/policy created successfully/i)).toBeInTheDocument();
116127
});
128+
129+
it("restores trigger condition builder rules from draft when going back to configure step", () => {
130+
localStorage.setItem(
131+
"stellarinsure-policy-draft",
132+
JSON.stringify({
133+
policyType: "weather",
134+
coverageAmount: "5000",
135+
premium: "120",
136+
triggerCondition: "temperature > 25 AND rainfall > 50",
137+
duration: "90",
138+
oracleProvider: "weatherlink-prime",
139+
}),
140+
);
141+
render(<CreatePolicyPageClient />);
142+
143+
expect(screen.getByRole("heading", { name: /review your policy/i })).toBeInTheDocument();
144+
145+
fireEvent.click(screen.getByRole("button", { name: /back/i }));
146+
147+
expect(screen.getByRole("heading", { name: /configure your policy/i })).toBeInTheDocument();
148+
149+
const triggerInput = screen.getByPlaceholderText("Trigger mock input") as HTMLInputElement;
150+
expect(triggerInput.value).toBe("temperature > 25 AND rainfall > 50");
151+
});
117152
});

frontend/src/app/create/create-page-client.tsx

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ import {
1919
import { useWallet } from "@/components/wallet-provider";
2020
import { useAutosave } from "@/hooks/use-autosave";
2121
import { useAppTranslation } from "@/i18n/provider";
22-
import { TriggerConditionBuilder } from "@/components/trigger-condition-builder";
22+
import { TriggerConditionBuilder, type ConditionRule, type Operator } from "@/components/trigger-condition-builder";
2323
import { PremiumEstimate, type PremiumBreakdown } from "@/components/premium-estimate";
2424
import { ValidationSummary, type ValidationError } from "@/components/validation-summary";
2525
import { signTransaction } from "@stellar/freighter-api";
@@ -58,6 +58,22 @@ const INITIAL_DRAFT: PolicyDraft = {
5858

5959

6060

61+
function parseConditionString(condition: string): ConditionRule[] | undefined {
62+
if (!condition || condition.trim() === "") return undefined;
63+
const parts = condition.split(" AND ").map((p) => p.trim()).filter(Boolean);
64+
if (parts.length === 0) return undefined;
65+
const rules: ConditionRule[] = [];
66+
const pattern = /^(\w+)\s*(>=|<=|>|<|=)\s*(.+)$/;
67+
for (const part of parts) {
68+
const match = part.match(pattern);
69+
if (!match) return undefined;
70+
const operator = match[2] as Operator;
71+
if (![">", "<", "=", ">=", "<="].includes(operator)) return undefined;
72+
rules.push({ field: match[1], operator, value: match[3].trim() });
73+
}
74+
return rules.length > 0 ? rules : undefined;
75+
}
76+
6177
const MAX_COVERAGE_AMOUNT = 1_000_000;
6278

6379
const ORACLE_PROVIDER_MAP: Record<PolicyType, OracleProvider[]> = {
@@ -523,6 +539,11 @@ export default function CreatePolicyPageClient() {
523539
oracleState === "ready" &&
524540
draft.oracleProvider.trim() !== "";
525541

542+
const triggerInitialRules = useMemo(
543+
() => parseConditionString(draft.triggerCondition),
544+
[draft.triggerCondition],
545+
);
546+
526547
const isWalletReady = isConnected && walletStatus !== "checking" && walletStatus !== "connecting";
527548

528549
return (
@@ -624,6 +645,7 @@ export default function CreatePolicyPageClient() {
624645
<div className="field field--full" id="trigger-input">
625646
<span className="field__label">{t("createPolicy.configSection.triggerLabel")}</span>
626647
<TriggerConditionBuilder
648+
initialRules={triggerInitialRules}
627649
onChange={(val) => updateDraft("triggerCondition", val)}
628650
/>
629651
<span className="field__hint">

0 commit comments

Comments
 (0)