Skip to content

Commit e495d64

Browse files
committed
fix: reset RegisterUpdateDialog state on close and block metadata shadowing
- useEffect resets id/name/automated/metadata/error when dialog closes, so reopening starts clean instead of showing stale inputs or errors. - Strip id/update_name/automated from the JSON metadata object and spread extras before the UI-controlled fields so user metadata cannot override validated top-level values. - Add regression test covering the reserved-key stripping.
1 parent ba35e12 commit e495d64

2 files changed

Lines changed: 39 additions & 8 deletions

File tree

src/components/RegisterUpdateDialog.test.tsx

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,23 @@ describe('RegisterUpdateDialog', () => {
2929
expect(onSubmit).not.toHaveBeenCalled();
3030
});
3131

32+
it('strips reserved keys from metadata so UI-validated fields win', async () => {
33+
const onSubmit = vi.fn().mockResolvedValue(undefined);
34+
render(<RegisterUpdateDialog open onClose={() => {}} onSubmit={onSubmit} />);
35+
fireEvent.change(screen.getByLabelText(/^id$/i), { target: { value: 'ui-id' } });
36+
fireEvent.change(screen.getByLabelText(/additional metadata/i), {
37+
target: { value: '{"id":"evil","update_name":"evil","automated":true,"extra":1}' },
38+
});
39+
fireEvent.click(screen.getByRole('button', { name: /^register$/i }));
40+
await waitFor(() =>
41+
expect(onSubmit).toHaveBeenCalledWith({
42+
id: 'ui-id',
43+
update_name: 'ui-id',
44+
extra: 1,
45+
})
46+
);
47+
});
48+
3249
it('submits merged body on valid input', async () => {
3350
const onSubmit = vi.fn().mockResolvedValue(undefined);
3451
render(<RegisterUpdateDialog open onClose={() => {}} onSubmit={onSubmit} />);

src/components/RegisterUpdateDialog.tsx

Lines changed: 22 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
// See the License for the specific language governing permissions and
1313
// limitations under the License.
1414

15-
import { useState } from 'react';
15+
import { useEffect, useState } from 'react';
1616
import { Button } from '@/components/ui/button';
1717
import { Input } from '@/components/ui/input';
1818
import { Label } from '@/components/ui/label';
@@ -41,6 +41,17 @@ export function RegisterUpdateDialog({ open, onClose, onSubmit }: Props) {
4141
const [error, setError] = useState<string | null>(null);
4242
const [submitting, setSubmitting] = useState(false);
4343

44+
useEffect(() => {
45+
if (!open) {
46+
setId('');
47+
setName('');
48+
setAutomated(false);
49+
setMetadata('{}');
50+
setError(null);
51+
setSubmitting(false);
52+
}
53+
}, [open]);
54+
4455
const handleSubmit = async () => {
4556
setError(null);
4657
if (!id.trim()) {
@@ -54,22 +65,25 @@ export function RegisterUpdateDialog({ open, onClose, onSubmit }: Props) {
5465
if (typeof parsed !== 'object' || parsed === null || Array.isArray(parsed)) {
5566
throw new Error('not an object');
5667
}
57-
extras = parsed as Record<string, unknown>;
68+
const { id: _i, update_name: _n, automated: _a, ...safe } = parsed as Record<string, unknown>;
69+
void _i;
70+
void _n;
71+
void _a;
72+
extras = safe;
5873
} catch {
5974
setError('invalid JSON in additional metadata');
6075
return;
6176
}
6277
}
63-
const body: RegisterUpdateBody = { id: id.trim(), ...extras };
64-
body.update_name = name.trim() || id.trim();
78+
const body: RegisterUpdateBody = {
79+
...extras,
80+
id: id.trim(),
81+
update_name: name.trim() || id.trim(),
82+
};
6583
if (automated) body.automated = true;
6684
setSubmitting(true);
6785
try {
6886
await onSubmit(body);
69-
setId('');
70-
setName('');
71-
setAutomated(false);
72-
setMetadata('{}');
7387
onClose();
7488
} catch (e) {
7589
setError(e instanceof Error ? e.message : String(e));

0 commit comments

Comments
 (0)