Skip to content

Commit 5d79afb

Browse files
committed
feat(explorer): validate immutable file number before download
1 parent 7bee80d commit 5d79afb

File tree

2 files changed

+121
-20
lines changed

2 files changed

+121
-20
lines changed
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
import { fireEvent, render, screen } from "@testing-library/react";
2+
import { test, fc } from "@fast-check/jest";
3+
import "@testing-library/jest-dom";
4+
import { DownloadImmutableFormInput } from "#/Artifacts/CardanoDbV2SnapshotsList/DownloadButton";
5+
6+
const maxImmutable = 100000;
7+
8+
function setup(max) {
9+
const utils = render(<DownloadImmutableFormInput max={max} />);
10+
return {
11+
// Note: in `fast-check` tests `screen.getByRole("spinbutton")` find two elements for a reason I don't understand, so
12+
// I'm using getAllByRole and selecting the first one to avoid the error.
13+
input: screen.getAllByRole("spinbutton")[0],
14+
...utils,
15+
};
16+
}
17+
18+
describe("DownloadImmutableFormInput", () => {
19+
it("Empty default to invalid", () => {
20+
const { input } = setup(maxImmutable);
21+
expect(input.checkValidity()).toBeFalsy();
22+
expect(input.value).toBe("");
23+
});
24+
25+
it("Setting empty string is invalid", () => {
26+
const { input } = setup(maxImmutable);
27+
fireEvent.change(input, { target: { value: "" } });
28+
expect(input.checkValidity()).toBeFalsy();
29+
expect(input.value).toBe("");
30+
});
31+
32+
test.prop({
33+
immutable_file_number: fc.nat({
34+
max: maxImmutable,
35+
}),
36+
})("Immutable below or equal to max allowed", ({ immutable_file_number }) => {
37+
const { input } = setup(maxImmutable);
38+
fireEvent.change(input, {
39+
// target: { value: `${immutable_file_number}` },
40+
target: { value: immutable_file_number },
41+
});
42+
43+
expect(input.checkValidity()).toBeTruthy();
44+
expect(input.value).toBe(`${immutable_file_number}`);
45+
});
46+
47+
test.prop({
48+
immutable_file_number: fc.oneof(fc.integer({ max: -1 }), fc.integer({ min: maxImmutable + 1 })),
49+
})("Immutable above max or below 0 is invalid", ({ immutable_file_number }) => {
50+
const { input } = setup(maxImmutable);
51+
fireEvent.change(input, {
52+
target: { value: immutable_file_number },
53+
});
54+
55+
expect(input.checkValidity()).toBeFalsy();
56+
});
57+
58+
test.prop({
59+
immutable_file_number: fc.string({ minLength: 1 }).filter((s) => isNaN(parseInt(s))),
60+
})("Non-number is invalid", ({ immutable_file_number }) => {
61+
const { input } = setup({ maxImmutable });
62+
fireEvent.change(input, {
63+
target: { value: immutable_file_number },
64+
});
65+
66+
expect(input.checkValidity()).toBeFalsy();
67+
});
68+
69+
test.prop({
70+
immutable_file_number: fc.float({ noInteger: true }),
71+
})("Float is invalid", ({ immutable_file_number }) => {
72+
const { input } = setup({ maxImmutable });
73+
fireEvent.change(input, {
74+
target: { value: immutable_file_number },
75+
});
76+
77+
expect(input.checkValidity()).toBeFalsy();
78+
});
79+
});

mithril-explorer/src/components/Artifacts/CardanoDbV2SnapshotsList/DownloadButton.js

Lines changed: 42 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,12 @@ function LocationsSelect({ ariaLabel, locations, value, onChange }) {
4444
);
4545
}
4646

47+
export function DownloadImmutableFormInput({ max, value, onChange }) {
48+
return (
49+
<Form.Control type="number" value={value} onChange={onChange} required min={0} max={max} />
50+
);
51+
}
52+
4753
export default function DownloadButton({ artifactUrl, ...props }) {
4854
const [show, setShow] = useState(false);
4955
const target = useRef(null);
@@ -54,6 +60,7 @@ export default function DownloadButton({ artifactUrl, ...props }) {
5460
const [digestLocationIndex, setDigestLocationIndex] = useState(0);
5561
const [immutableLocationIndex, setImmutableLocationIndex] = useState(0);
5662
const [ancillaryLocationIndex, setAncillaryLocationIndex] = useState(0);
63+
const [showImmutableDownloadValidation, setShowImmutableDownloadValidation] = useState(false);
5764

5865
const handleClose = () => setShow(false);
5966

@@ -83,11 +90,20 @@ export default function DownloadButton({ artifactUrl, ...props }) {
8390
window.open(uri, "_blank", "noopener");
8491
}
8592

86-
function downloadImmutable() {
87-
const location = artifact?.locations.immutables[immutableLocationIndex];
93+
function downloadImmutable(event) {
94+
// Prevent page refresh
95+
event.preventDefault();
96+
const form = event.target;
97+
98+
if (form.checkValidity() === true) {
99+
const location = artifact?.locations.immutables[immutableLocationIndex];
88100

89-
if (location?.type === "cloud_storage") {
90-
directDownload(getImmutableUrlFromTemplate(location.uri.Template, immutableFileNumber));
101+
if (location?.type === "cloud_storage") {
102+
directDownload(getImmutableUrlFromTemplate(location.uri.Template, immutableFileNumber));
103+
}
104+
setShowImmutableDownloadValidation(false);
105+
} else {
106+
setShowImmutableDownloadValidation(true);
91107
}
92108
}
93109

@@ -141,22 +157,28 @@ export default function DownloadButton({ artifactUrl, ...props }) {
141157
</Alert>
142158

143159
<Form.Text>Immutable (max: {maxImmutableFileNumber})</Form.Text>
144-
<InputGroup>
145-
<Form.Control
146-
type="text"
147-
value={immutableFileNumber}
148-
onChange={(e) => setImmutableFileNumber(e.target.value)}
149-
/>
150-
<LocationsSelect
151-
ariaLabel="Immutable locations"
152-
locations={artifact?.locations.immutables}
153-
onChange={(e) => setImmutableLocationIndex(e.target.value)}
154-
value={immutableLocationIndex}
155-
/>
156-
<Button variant="primary" onClick={downloadImmutable}>
157-
<DownloadIcon />
158-
</Button>
159-
</InputGroup>
160+
<Form
161+
noValidate
162+
onSubmit={downloadImmutable}
163+
validated={showImmutableDownloadValidation}>
164+
<InputGroup>
165+
<DownloadImmutableFormInput
166+
type="text"
167+
max={maxImmutableFileNumber}
168+
value={immutableFileNumber}
169+
onChange={(e) => setImmutableFileNumber(e.target.value)}
170+
/>
171+
<LocationsSelect
172+
ariaLabel="Immutable locations"
173+
locations={artifact?.locations.immutables}
174+
onChange={(e) => setImmutableLocationIndex(e.target.value)}
175+
value={immutableLocationIndex}
176+
/>
177+
<Button variant="primary" type="submit">
178+
<DownloadIcon />
179+
</Button>
180+
</InputGroup>
181+
</Form>
160182

161183
<Form.Text>Digests</Form.Text>
162184
<InputGroup>

0 commit comments

Comments
 (0)