@johnnyboy11234/esg-idea-contest 0.3.1 → 0.3.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/backend/api/openapi.yaml +36 -0
- package/backend/cmd/server/main.go +1 -0
- package/backend/internal/account/internal/passwords/NOTICE +36 -0
- package/backend/internal/account/internal/passwords/common-passwords.txt +10001 -0
- package/backend/internal/account/internal/passwords/passwords.go +37 -0
- package/backend/internal/account/internal/passwords/passwords_test.go +35 -0
- package/backend/internal/account/password.go +8 -2
- package/backend/internal/account/sequence.go +54 -0
- package/backend/internal/account/sequence_test.go +38 -0
- package/backend/internal/api/api.gen.go +38 -0
- package/backend/internal/idea/create.go +5 -1
- package/backend/internal/idea/create_integration_test.go +33 -3
- package/backend/internal/idea/download.go +193 -0
- package/backend/internal/idea/download_integration_test.go +311 -0
- package/backend/internal/idea/download_test.go +25 -0
- package/backend/internal/idea/handler.go +3 -0
- package/backend/internal/idea/service.go +2 -1
- package/backend/internal/server/api.go +5 -0
- package/backend/internal/server/binding.go +4 -0
- package/backend/internal/server/server.go +20 -1
- package/backend/internal/server/slides_test.go +82 -0
- package/backend/internal/shared/web/transport.go +4 -3
- package/backend/test/arch/arch_test.go +5 -1
- package/backend/test/matrix/matrix_integration_test.go +6 -0
- package/docs/Architecture/Architecture.md +1 -1
- package/docs/Architecture/Backend.md +2 -1
- package/frontend/Dockerfile +2 -0
- package/frontend/package-lock.json +1 -0
- package/frontend/package.json +1 -0
- package/frontend/src/app/pages/ideaCreate.test.tsx +308 -0
- package/frontend/src/app/pages/ideas.tsx +23 -9
- package/frontend/src/app/pages/login.test.tsx +38 -1
- package/frontend/src/app/pages/register.test.tsx +32 -0
- package/frontend/src/features/auth/LoginForm.tsx +21 -3
- package/frontend/src/features/auth/PasswordChecklist.test.tsx +73 -0
- package/frontend/src/features/auth/PasswordChecklist.tsx +96 -0
- package/frontend/src/features/auth/RegisterForm.tsx +11 -2
- package/frontend/src/features/auth/common-passwords.ts +31 -0
- package/frontend/src/features/auth/password-policy.test.ts +64 -0
- package/frontend/src/features/auth/password-policy.ts +168 -0
- package/frontend/src/features/auth/useCountdown.test.tsx +33 -0
- package/frontend/src/features/auth/useCountdown.ts +28 -0
- package/frontend/src/features/idea/CreateIdeaForm.tsx +279 -0
- package/frontend/src/features/idea/create.ts +123 -0
- package/frontend/src/shared/api/types.generated.ts +319 -1
- package/frontend/src/shared/i18n/locales/en.ts +45 -4
- package/frontend/src/shared/i18n/locales/zh-TW.ts +45 -4
- package/frontend/src/shared/slides/index.ts +74 -0
- package/frontend/src/shared/slides/slides.test.ts +51 -0
- package/frontend/src/shared/ui/copy-button.tsx +52 -0
- package/frontend/src/shared/ui/native-select.tsx +27 -0
- package/frontend/src/shared/ui/textarea.tsx +19 -0
- package/frontend/src/test-setup.ts +37 -2
- package/frontend/vite.config.ts +3 -1
- package/package.json +1 -1
- package/testdata/password.json +164 -7
- package/testdata/permission-matrix.json +23 -0
- package/testdata/schema/permission-matrix.schema.json +1 -1
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { CheckIcon, CopyIcon } from "lucide-react"
|
|
2
|
+
import { useEffect, useState } from "react"
|
|
3
|
+
import { Button } from "./button"
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Copies `value` to the clipboard. The button has an accessible name of its own
|
|
7
|
+
* (`label`); the result is announced in a polite live region (WCAG 4.1.3).
|
|
8
|
+
*/
|
|
9
|
+
export function CopyButton({
|
|
10
|
+
value,
|
|
11
|
+
label,
|
|
12
|
+
copiedText,
|
|
13
|
+
failedText,
|
|
14
|
+
}: {
|
|
15
|
+
value: string
|
|
16
|
+
label: string
|
|
17
|
+
copiedText: string
|
|
18
|
+
failedText: string
|
|
19
|
+
}) {
|
|
20
|
+
const [state, setState] = useState<"idle" | "copied" | "failed">("idle")
|
|
21
|
+
useEffect(() => {
|
|
22
|
+
if (state === "idle") return
|
|
23
|
+
const id = setTimeout(() => setState("idle"), 2000)
|
|
24
|
+
return () => clearTimeout(id)
|
|
25
|
+
}, [state])
|
|
26
|
+
|
|
27
|
+
const copy = async () => {
|
|
28
|
+
try {
|
|
29
|
+
await navigator.clipboard.writeText(value)
|
|
30
|
+
setState("copied")
|
|
31
|
+
} catch {
|
|
32
|
+
setState("failed")
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
return (
|
|
37
|
+
<>
|
|
38
|
+
<Button
|
|
39
|
+
type="button"
|
|
40
|
+
variant="outline"
|
|
41
|
+
size="icon-sm"
|
|
42
|
+
aria-label={label}
|
|
43
|
+
onClick={() => void copy()}
|
|
44
|
+
>
|
|
45
|
+
{state === "copied" ? <CheckIcon aria-hidden="true" /> : <CopyIcon aria-hidden="true" />}
|
|
46
|
+
</Button>
|
|
47
|
+
<span role="status" className="sr-only">
|
|
48
|
+
{state === "copied" ? copiedText : state === "failed" ? failedText : ""}
|
|
49
|
+
</span>
|
|
50
|
+
</>
|
|
51
|
+
)
|
|
52
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import * as React from "react"
|
|
2
|
+
import { ChevronDownIcon } from "lucide-react"
|
|
3
|
+
import { cn } from "./utils"
|
|
4
|
+
|
|
5
|
+
/** A styled native <select>: keyboard, screen-reader and mobile behaviour stay the browser's. */
|
|
6
|
+
function NativeSelect({ className, ...props }: React.ComponentProps<"select">) {
|
|
7
|
+
return (
|
|
8
|
+
<div className="relative w-full">
|
|
9
|
+
<select
|
|
10
|
+
data-slot="native-select"
|
|
11
|
+
className={cn(
|
|
12
|
+
"h-9 w-full min-w-0 appearance-none rounded-md border border-input bg-transparent py-1 pr-9 pl-3 text-base shadow-xs transition-[color,box-shadow] outline-none disabled:cursor-not-allowed disabled:opacity-50 md:text-sm dark:bg-input/30",
|
|
13
|
+
"focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50",
|
|
14
|
+
"aria-invalid:border-destructive aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40",
|
|
15
|
+
className
|
|
16
|
+
)}
|
|
17
|
+
{...props}
|
|
18
|
+
/>
|
|
19
|
+
<ChevronDownIcon
|
|
20
|
+
aria-hidden="true"
|
|
21
|
+
className="pointer-events-none absolute top-1/2 right-3 size-4 -translate-y-1/2 opacity-50"
|
|
22
|
+
/>
|
|
23
|
+
</div>
|
|
24
|
+
)
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export { NativeSelect }
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import * as React from "react"
|
|
2
|
+
import { cn } from "./utils"
|
|
3
|
+
|
|
4
|
+
function Textarea({ className, ...props }: React.ComponentProps<"textarea">) {
|
|
5
|
+
return (
|
|
6
|
+
<textarea
|
|
7
|
+
data-slot="textarea"
|
|
8
|
+
className={cn(
|
|
9
|
+
"flex field-sizing-content min-h-16 w-full rounded-md border border-input bg-transparent px-3 py-2 text-base shadow-xs transition-[color,box-shadow] outline-none placeholder:text-muted-foreground disabled:cursor-not-allowed disabled:opacity-50 md:text-sm dark:bg-input/30",
|
|
10
|
+
"focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50",
|
|
11
|
+
"aria-invalid:border-destructive aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40",
|
|
12
|
+
className
|
|
13
|
+
)}
|
|
14
|
+
{...props}
|
|
15
|
+
/>
|
|
16
|
+
)
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export { Textarea }
|
|
@@ -1,6 +1,41 @@
|
|
|
1
1
|
import '@testing-library/jest-dom/vitest';
|
|
2
|
+
import { File as NodeFile } from 'node:buffer';
|
|
3
|
+
import { FormData as NodeFormData, Response as NodeResponse } from 'undici';
|
|
2
4
|
import { server } from '@/shared/api/mocks/server';
|
|
3
5
|
|
|
4
|
-
|
|
6
|
+
/**
|
|
7
|
+
* jsdom replaces FormData/File with its own classes, which Node's fetch cannot
|
|
8
|
+
* serialise (the multipart body is never written, so MSW fails with "Premature
|
|
9
|
+
* close"). Real browsers send a FormData natively; here the jsdom one is encoded
|
|
10
|
+
* to multipart bytes (by undici's Response) just before the request reaches MSW.
|
|
11
|
+
* Installed after `server.listen()` so that it sits on top of MSW's own fetch
|
|
12
|
+
* interceptor.
|
|
13
|
+
*/
|
|
14
|
+
async function encodeMultipart(body: FormData): Promise<{ bytes: ArrayBuffer; contentType: string }> {
|
|
15
|
+
const out = new NodeFormData();
|
|
16
|
+
for (const [key, value] of body.entries()) {
|
|
17
|
+
if (typeof value === 'string') out.append(key, value);
|
|
18
|
+
else out.append(key, new NodeFile([await value.arrayBuffer()], value.name, { type: value.type }));
|
|
19
|
+
}
|
|
20
|
+
const res = new NodeResponse(out);
|
|
21
|
+
return { bytes: await res.arrayBuffer(), contentType: res.headers.get('content-type') ?? '' };
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
let mswFetch: typeof fetch;
|
|
25
|
+
|
|
26
|
+
beforeAll(() => {
|
|
27
|
+
server.listen({ onUnhandledFrame: 'error' });
|
|
28
|
+
mswFetch = globalThis.fetch;
|
|
29
|
+
globalThis.fetch = async (input, init) => {
|
|
30
|
+
if (!(init?.body instanceof FormData)) return mswFetch(input, init);
|
|
31
|
+
const { bytes, contentType } = await encodeMultipart(init.body);
|
|
32
|
+
const headers = new Headers(init.headers);
|
|
33
|
+
headers.set('Content-Type', contentType);
|
|
34
|
+
return mswFetch(input, { ...init, headers, body: bytes });
|
|
35
|
+
};
|
|
36
|
+
});
|
|
5
37
|
afterEach(() => server.resetHandlers());
|
|
6
|
-
afterAll(() =>
|
|
38
|
+
afterAll(() => {
|
|
39
|
+
globalThis.fetch = mswFetch;
|
|
40
|
+
server.close();
|
|
41
|
+
});
|
package/frontend/vite.config.ts
CHANGED
|
@@ -5,10 +5,12 @@ import { defineConfig } from 'vitest/config';
|
|
|
5
5
|
|
|
6
6
|
const src = fileURLToPath(new URL('./src', import.meta.url));
|
|
7
7
|
const testdata = fileURLToPath(new URL('../testdata', import.meta.url));
|
|
8
|
+
// The backend's bundled common-password list (PRD §6.4): one file for both sides, lazy-loaded by the register page.
|
|
9
|
+
const passwords = fileURLToPath(new URL('../backend/internal/account/internal/passwords', import.meta.url));
|
|
8
10
|
|
|
9
11
|
export default defineConfig({
|
|
10
12
|
plugins: [react(), tailwindcss()],
|
|
11
|
-
resolve: { alias: { '@': src, testdata } },
|
|
13
|
+
resolve: { alias: { '@': src, testdata, passwords } },
|
|
12
14
|
server: {
|
|
13
15
|
fs: { allow: ['..'] },
|
|
14
16
|
// The app calls same-origin /api/v1; in dev the Go backend (ESG_PUBLIC_ADDR, default :8080) answers it.
|
package/package.json
CHANGED
package/testdata/password.json
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
{
|
|
2
|
-
"description": "Shared by backend (Go) and frontend (TS). password and empNo are raw strings; empNo is \"\" when the submitted employee ID is malformed (CONTAINS_EMP_NO is then skipped). wantRules lists every violated rule id in PRD 6.4 table order ([] = valid).
|
|
2
|
+
"description": "Shared by backend (Go) and frontend (TS). password and empNo are raw strings; empNo is \"\" when the submitted employee ID is malformed (CONTAINS_EMP_NO is then skipped). wantRules lists every violated rule id in PRD 6.4 table order ([] = valid). One substring may violate several rules (\"1234\" is KEYBOARD_RUN and SEQUENCE). COMMON_PASSWORD uses backend/internal/account/internal/passwords/common-passwords.txt (whole password, case-insensitive, nothing trimmed). PRD 6.4.",
|
|
3
3
|
"cases": [
|
|
4
4
|
{
|
|
5
5
|
"id": "valid-typical",
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
},
|
|
10
10
|
{
|
|
11
11
|
"id": "valid-min-length-12",
|
|
12
|
-
"password": "
|
|
12
|
+
"password": "Azkqmvrt1!xY",
|
|
13
13
|
"empNo": "",
|
|
14
14
|
"wantRules": []
|
|
15
15
|
},
|
|
@@ -51,7 +51,7 @@
|
|
|
51
51
|
},
|
|
52
52
|
{
|
|
53
53
|
"id": "length-11",
|
|
54
|
-
"password": "
|
|
54
|
+
"password": "Azkqmvrt1!x",
|
|
55
55
|
"empNo": "",
|
|
56
56
|
"wantRules": [
|
|
57
57
|
"LENGTH"
|
|
@@ -125,7 +125,7 @@
|
|
|
125
125
|
},
|
|
126
126
|
{
|
|
127
127
|
"id": "complexity-two-classes-lower-digit",
|
|
128
|
-
"password": "
|
|
128
|
+
"password": "azkqmvrtjx12",
|
|
129
129
|
"empNo": "",
|
|
130
130
|
"wantRules": [
|
|
131
131
|
"COMPLEXITY"
|
|
@@ -149,7 +149,7 @@
|
|
|
149
149
|
},
|
|
150
150
|
{
|
|
151
151
|
"id": "complexity-non-ascii-is-no-class",
|
|
152
|
-
"password": "
|
|
152
|
+
"password": "azkqmvrtjxp1é",
|
|
153
153
|
"empNo": "",
|
|
154
154
|
"wantRules": [
|
|
155
155
|
"CHARSET",
|
|
@@ -239,6 +239,7 @@
|
|
|
239
239
|
"LENGTH",
|
|
240
240
|
"COMPLEXITY",
|
|
241
241
|
"KEYBOARD_RUN",
|
|
242
|
+
"SEQUENCE",
|
|
242
243
|
"REPEAT",
|
|
243
244
|
"CONTAINS_EMP_NO"
|
|
244
245
|
]
|
|
@@ -251,6 +252,7 @@
|
|
|
251
252
|
"CHARSET",
|
|
252
253
|
"COMPLEXITY",
|
|
253
254
|
"KEYBOARD_RUN",
|
|
255
|
+
"SEQUENCE",
|
|
254
256
|
"REPEAT",
|
|
255
257
|
"CONTAINS_EMP_NO"
|
|
256
258
|
]
|
|
@@ -260,7 +262,8 @@
|
|
|
260
262
|
"password": "Xk9-1234-Pmzj",
|
|
261
263
|
"empNo": "",
|
|
262
264
|
"wantRules": [
|
|
263
|
-
"KEYBOARD_RUN"
|
|
265
|
+
"KEYBOARD_RUN",
|
|
266
|
+
"SEQUENCE"
|
|
264
267
|
]
|
|
265
268
|
},
|
|
266
269
|
{
|
|
@@ -300,7 +303,8 @@
|
|
|
300
303
|
"password": "Zq-4321-Mj8P",
|
|
301
304
|
"empNo": "",
|
|
302
305
|
"wantRules": [
|
|
303
|
-
"KEYBOARD_RUN"
|
|
306
|
+
"KEYBOARD_RUN",
|
|
307
|
+
"SEQUENCE"
|
|
304
308
|
]
|
|
305
309
|
},
|
|
306
310
|
{
|
|
@@ -418,8 +422,161 @@
|
|
|
418
422
|
"empNo": "",
|
|
419
423
|
"wantRules": [
|
|
420
424
|
"KEYBOARD_RUN",
|
|
425
|
+
"SEQUENCE",
|
|
421
426
|
"REPEAT"
|
|
422
427
|
]
|
|
428
|
+
},
|
|
429
|
+
{
|
|
430
|
+
"id": "sequence-alpha-ascending",
|
|
431
|
+
"password": "Zq-abcd-M81x",
|
|
432
|
+
"empNo": "",
|
|
433
|
+
"wantRules": [
|
|
434
|
+
"SEQUENCE"
|
|
435
|
+
]
|
|
436
|
+
},
|
|
437
|
+
{
|
|
438
|
+
"id": "sequence-alpha-descending",
|
|
439
|
+
"password": "Zq-dcba-M81x",
|
|
440
|
+
"empNo": "",
|
|
441
|
+
"wantRules": [
|
|
442
|
+
"SEQUENCE"
|
|
443
|
+
]
|
|
444
|
+
},
|
|
445
|
+
{
|
|
446
|
+
"id": "sequence-case-insensitive",
|
|
447
|
+
"password": "Zq-aBcD-M81x",
|
|
448
|
+
"empNo": "",
|
|
449
|
+
"wantRules": [
|
|
450
|
+
"SEQUENCE"
|
|
451
|
+
]
|
|
452
|
+
},
|
|
453
|
+
{
|
|
454
|
+
"id": "sequence-digits-not-a-keyboard-run",
|
|
455
|
+
"password": "Zq-0123-Mkpx",
|
|
456
|
+
"empNo": "",
|
|
457
|
+
"wantRules": [
|
|
458
|
+
"SEQUENCE"
|
|
459
|
+
]
|
|
460
|
+
},
|
|
461
|
+
{
|
|
462
|
+
"id": "sequence-digits-descending-not-a-keyboard-run",
|
|
463
|
+
"password": "Zq-3210-Mkpx",
|
|
464
|
+
"empNo": "",
|
|
465
|
+
"wantRules": [
|
|
466
|
+
"SEQUENCE"
|
|
467
|
+
]
|
|
468
|
+
},
|
|
469
|
+
{
|
|
470
|
+
"id": "sequence-longer-run-reported-once",
|
|
471
|
+
"password": "Zq-abcdefg-M1",
|
|
472
|
+
"empNo": "",
|
|
473
|
+
"wantRules": [
|
|
474
|
+
"SEQUENCE"
|
|
475
|
+
]
|
|
476
|
+
},
|
|
477
|
+
{
|
|
478
|
+
"id": "sequence-three-ok",
|
|
479
|
+
"password": "Zq-abc-M81xPk",
|
|
480
|
+
"empNo": "",
|
|
481
|
+
"wantRules": []
|
|
482
|
+
},
|
|
483
|
+
{
|
|
484
|
+
"id": "sequence-alpha-does-not-wrap",
|
|
485
|
+
"password": "Zq-xyza-M81p",
|
|
486
|
+
"empNo": "",
|
|
487
|
+
"wantRules": []
|
|
488
|
+
},
|
|
489
|
+
{
|
|
490
|
+
"id": "sequence-digits-do-not-wrap",
|
|
491
|
+
"password": "Zq-9012-Mkpx",
|
|
492
|
+
"empNo": "",
|
|
493
|
+
"wantRules": []
|
|
494
|
+
},
|
|
495
|
+
{
|
|
496
|
+
"id": "sequence-direction-change-ok",
|
|
497
|
+
"password": "Zq-abcba-M81x",
|
|
498
|
+
"empNo": "",
|
|
499
|
+
"wantRules": []
|
|
500
|
+
},
|
|
501
|
+
{
|
|
502
|
+
"id": "sequence-repeat-breaks-run",
|
|
503
|
+
"password": "Zq-abbc-M81xP",
|
|
504
|
+
"empNo": "",
|
|
505
|
+
"wantRules": []
|
|
506
|
+
},
|
|
507
|
+
{
|
|
508
|
+
"id": "sequence-letters-and-digits-do-not-join",
|
|
509
|
+
"password": "Zq-89ab-Mkpx",
|
|
510
|
+
"empNo": "",
|
|
511
|
+
"wantRules": []
|
|
512
|
+
},
|
|
513
|
+
{
|
|
514
|
+
"id": "sequence-ascii-neighbour-does-not-join",
|
|
515
|
+
"password": "Zq-xyz{-Mk81",
|
|
516
|
+
"empNo": "",
|
|
517
|
+
"wantRules": []
|
|
518
|
+
},
|
|
519
|
+
{
|
|
520
|
+
"id": "common-exact",
|
|
521
|
+
"password": "password",
|
|
522
|
+
"empNo": "",
|
|
523
|
+
"wantRules": [
|
|
524
|
+
"LENGTH",
|
|
525
|
+
"COMPLEXITY",
|
|
526
|
+
"COMMON_PASSWORD"
|
|
527
|
+
]
|
|
528
|
+
},
|
|
529
|
+
{
|
|
530
|
+
"id": "common-case-insensitive",
|
|
531
|
+
"password": "Films+Pic+Galeries",
|
|
532
|
+
"empNo": "",
|
|
533
|
+
"wantRules": [
|
|
534
|
+
"COMMON_PASSWORD"
|
|
535
|
+
]
|
|
536
|
+
},
|
|
537
|
+
{
|
|
538
|
+
"id": "common-lower-case-entry",
|
|
539
|
+
"password": "films+pic+galeries",
|
|
540
|
+
"empNo": "",
|
|
541
|
+
"wantRules": [
|
|
542
|
+
"COMPLEXITY",
|
|
543
|
+
"COMMON_PASSWORD"
|
|
544
|
+
]
|
|
545
|
+
},
|
|
546
|
+
{
|
|
547
|
+
"id": "common-upper-case",
|
|
548
|
+
"password": "PASSWORD",
|
|
549
|
+
"empNo": "",
|
|
550
|
+
"wantRules": [
|
|
551
|
+
"LENGTH",
|
|
552
|
+
"COMPLEXITY",
|
|
553
|
+
"COMMON_PASSWORD"
|
|
554
|
+
]
|
|
555
|
+
},
|
|
556
|
+
{
|
|
557
|
+
"id": "common-not-trimmed",
|
|
558
|
+
"password": " Films+Pic+Galeries",
|
|
559
|
+
"empNo": "",
|
|
560
|
+
"wantRules": []
|
|
561
|
+
},
|
|
562
|
+
{
|
|
563
|
+
"id": "common-entry-inside-longer-password-ok",
|
|
564
|
+
"password": "Films+Pic+Galeries1",
|
|
565
|
+
"empNo": "",
|
|
566
|
+
"wantRules": []
|
|
567
|
+
},
|
|
568
|
+
{
|
|
569
|
+
"id": "table-order-common-and-runs",
|
|
570
|
+
"password": "123456",
|
|
571
|
+
"empNo": "123456",
|
|
572
|
+
"wantRules": [
|
|
573
|
+
"LENGTH",
|
|
574
|
+
"COMPLEXITY",
|
|
575
|
+
"COMMON_PASSWORD",
|
|
576
|
+
"KEYBOARD_RUN",
|
|
577
|
+
"SEQUENCE",
|
|
578
|
+
"CONTAINS_EMP_NO"
|
|
579
|
+
]
|
|
423
580
|
}
|
|
424
581
|
]
|
|
425
582
|
}
|
|
@@ -116,6 +116,29 @@
|
|
|
116
116
|
"admin": { "status": 403, "code": "FORBIDDEN" }
|
|
117
117
|
}
|
|
118
118
|
},
|
|
119
|
+
{
|
|
120
|
+
"id": "download-slides-missing",
|
|
121
|
+
"operation": "download slides of a missing Idea (uniform 404; cells for an existing Idea are in internal/idea/download_integration_test.go)",
|
|
122
|
+
"method": "GET",
|
|
123
|
+
"path": "/api/v1/ideas/00000000-0000-4000-8000-000000000000/slides",
|
|
124
|
+
"expect": {
|
|
125
|
+
"visitor": { "status": 401, "code": "UNAUTHENTICATED" },
|
|
126
|
+
"user": { "status": 404, "code": "NOT_FOUND" },
|
|
127
|
+
"admin": { "status": 404, "code": "NOT_FOUND" }
|
|
128
|
+
}
|
|
129
|
+
},
|
|
130
|
+
{
|
|
131
|
+
"id": "download-slides-head",
|
|
132
|
+
"operation": "HEAD on the slide download: 405, never the GET handler (D47)",
|
|
133
|
+
"method": "HEAD",
|
|
134
|
+
"path": "/api/v1/ideas/00000000-0000-4000-8000-000000000000/slides",
|
|
135
|
+
"outsideSpec": true,
|
|
136
|
+
"expect": {
|
|
137
|
+
"visitor": { "status": 401, "code": "UNAUTHENTICATED" },
|
|
138
|
+
"user": { "status": 405, "code": "METHOD_NOT_ALLOWED" },
|
|
139
|
+
"admin": { "status": 405, "code": "METHOD_NOT_ALLOWED" }
|
|
140
|
+
}
|
|
141
|
+
},
|
|
119
142
|
{
|
|
120
143
|
"id": "order-logout-no-csrf",
|
|
121
144
|
"operation": "check order: a write without a CSRF token",
|
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
"properties": {
|
|
19
19
|
"id": { "type": "string", "pattern": "^[a-z0-9]+(-[a-z0-9]+)*$" },
|
|
20
20
|
"operation": { "type": "string", "minLength": 1 },
|
|
21
|
-
"method": { "enum": ["GET", "POST", "PUT", "PATCH", "DELETE"] },
|
|
21
|
+
"method": { "enum": ["GET", "HEAD", "POST", "PUT", "PATCH", "DELETE"] },
|
|
22
22
|
"path": { "type": "string", "pattern": "^/api/v1/" },
|
|
23
23
|
"body": { "type": "object" },
|
|
24
24
|
"multipart": {
|