@johnnyboy11234/esg-idea-contest 0.3.0 → 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.
Files changed (138) hide show
  1. package/backend/api/openapi.yaml +119 -0
  2. package/backend/cmd/server/main.go +36 -2
  3. package/backend/db/migration/V005__idea.sql +49 -0
  4. package/backend/db/migration/V006__login_lock.sql +15 -0
  5. package/backend/db/migration/V007__login_attempt.sql +15 -0
  6. package/backend/db/migration/V008__pending_file_deletion.sql +27 -0
  7. package/backend/db/migration/V009__login_throttle_audit.sql +13 -0
  8. package/backend/go.mod +17 -3
  9. package/backend/go.sum +48 -7
  10. package/backend/internal/account/account_test.go +6 -6
  11. package/backend/internal/account/integration_test.go +45 -9
  12. package/backend/internal/account/internal/passwords/NOTICE +36 -0
  13. package/backend/internal/account/internal/passwords/common-passwords.txt +10001 -0
  14. package/backend/internal/account/internal/passwords/passwords.go +37 -0
  15. package/backend/internal/account/internal/passwords/passwords_test.go +35 -0
  16. package/backend/internal/account/keyboard.go +75 -0
  17. package/backend/internal/account/keyboard_test.go +43 -0
  18. package/backend/internal/account/logging_integration_test.go +1 -1
  19. package/backend/internal/account/password.go +131 -0
  20. package/backend/internal/account/sequence.go +54 -0
  21. package/backend/internal/account/sequence_test.go +38 -0
  22. package/backend/internal/account/validate.go +4 -21
  23. package/backend/internal/api/api.gen.go +262 -0
  24. package/backend/internal/auth/internal/store/login_attempt.go +53 -0
  25. package/backend/internal/auth/internal/store/login_lock.go +77 -0
  26. package/backend/internal/auth/internal/store/login_throttle_audit.go +42 -0
  27. package/backend/internal/auth/internal/store/session.go +15 -7
  28. package/backend/internal/auth/login.go +16 -4
  29. package/backend/internal/auth/login_integration_test.go +14 -1
  30. package/backend/internal/auth/login_ip_integration_test.go +153 -0
  31. package/backend/internal/auth/login_lock_integration_test.go +205 -0
  32. package/backend/internal/auth/login_test.go +15 -0
  33. package/backend/internal/auth/login_throttle_audit_integration_test.go +88 -0
  34. package/backend/internal/auth/purge.go +93 -0
  35. package/backend/internal/auth/purge_integration_test.go +220 -0
  36. package/backend/internal/auth/purge_test.go +51 -0
  37. package/backend/internal/auth/session.go +0 -6
  38. package/backend/internal/auth/session_integration_test.go +3 -3
  39. package/backend/internal/auth/throttle.go +122 -0
  40. package/backend/internal/deadline/gate.go +80 -0
  41. package/backend/internal/deadline/gate_integration_test.go +67 -0
  42. package/backend/internal/deadline/gate_test.go +61 -0
  43. package/backend/internal/idea/create.go +356 -0
  44. package/backend/internal/idea/create_integration_test.go +680 -0
  45. package/backend/internal/idea/download.go +193 -0
  46. package/backend/internal/idea/download_integration_test.go +311 -0
  47. package/backend/internal/idea/download_test.go +25 -0
  48. package/backend/internal/idea/handler.go +151 -0
  49. package/backend/internal/idea/handler_integration_test.go +107 -0
  50. package/backend/internal/idea/integration_test.go +149 -0
  51. package/backend/internal/idea/internal/store/list.go +146 -0
  52. package/backend/internal/idea/internal/store/list_test.go +33 -0
  53. package/backend/internal/idea/internal/store/store.go +166 -0
  54. package/backend/internal/idea/list.go +166 -0
  55. package/backend/internal/idea/list_integration_test.go +252 -0
  56. package/backend/internal/idea/list_test.go +96 -0
  57. package/backend/internal/idea/service.go +54 -0
  58. package/backend/internal/server/api.go +22 -0
  59. package/backend/internal/server/binding.go +4 -0
  60. package/backend/internal/server/public.go +1 -0
  61. package/backend/internal/server/server.go +20 -1
  62. package/backend/internal/server/slides.go +21 -0
  63. package/backend/internal/server/slides_test.go +82 -0
  64. package/backend/internal/shared/config/config.go +44 -1
  65. package/backend/internal/shared/config/config_test.go +21 -13
  66. package/backend/internal/shared/db/intx_integration_test.go +179 -0
  67. package/backend/internal/shared/dbtest/dbtest.go +21 -3
  68. package/backend/internal/shared/dbtest/faults.go +162 -0
  69. package/backend/internal/shared/job/job.go +54 -0
  70. package/backend/internal/shared/job/job_test.go +105 -0
  71. package/backend/internal/shared/web/multipart.go +175 -0
  72. package/backend/internal/shared/web/multipart_test.go +169 -0
  73. package/backend/internal/shared/web/problem.go +1 -0
  74. package/backend/internal/shared/web/request.go +42 -1
  75. package/backend/internal/shared/web/transport.go +4 -3
  76. package/backend/internal/shared/web/web_test.go +36 -0
  77. package/backend/internal/storage/expectedfilename.go +43 -0
  78. package/backend/internal/storage/expectedfilename_test.go +16 -0
  79. package/backend/internal/storage/internal/store/pending.go +41 -0
  80. package/backend/internal/storage/minio.go +139 -0
  81. package/backend/internal/storage/minio_integration_test.go +87 -0
  82. package/backend/internal/storage/minio_test.go +84 -0
  83. package/backend/internal/storage/pending.go +144 -0
  84. package/backend/internal/storage/pending_integration_test.go +324 -0
  85. package/backend/internal/storage/pending_test.go +23 -0
  86. package/backend/internal/storage/validator.go +100 -0
  87. package/backend/internal/storage/validator_test.go +95 -0
  88. package/backend/test/arch/arch_test.go +5 -1
  89. package/backend/test/contract/expected_filename_test.go +60 -0
  90. package/backend/test/contract/password_test.go +46 -0
  91. package/backend/test/matrix/load_test.go +12 -0
  92. package/backend/test/matrix/matrix_integration_test.go +92 -5
  93. package/backend/test/migration/pending_file_deletion_test.go +46 -0
  94. package/docs/API/api-endpoints.md +1 -1
  95. package/docs/API/error-codes.md +1 -1
  96. package/docs/Architecture/Architecture.md +1 -1
  97. package/docs/Architecture/Backend.md +2 -1
  98. package/frontend/Dockerfile +2 -0
  99. package/frontend/package-lock.json +1 -0
  100. package/frontend/package.json +1 -0
  101. package/frontend/src/app/App.tsx +11 -2
  102. package/frontend/src/app/pages/ideaCreate.test.tsx +308 -0
  103. package/frontend/src/app/pages/ideas.tsx +23 -9
  104. package/frontend/src/app/pages/login.test.tsx +38 -1
  105. package/frontend/src/app/pages/register.test.tsx +32 -0
  106. package/frontend/src/features/auth/IdleWarning.test.tsx +82 -0
  107. package/frontend/src/features/auth/IdleWarning.tsx +66 -0
  108. package/frontend/src/features/auth/LoginForm.tsx +21 -3
  109. package/frontend/src/features/auth/PasswordChecklist.test.tsx +73 -0
  110. package/frontend/src/features/auth/PasswordChecklist.tsx +96 -0
  111. package/frontend/src/features/auth/RegisterForm.tsx +11 -2
  112. package/frontend/src/features/auth/common-passwords.ts +31 -0
  113. package/frontend/src/features/auth/password-policy.test.ts +64 -0
  114. package/frontend/src/features/auth/password-policy.ts +168 -0
  115. package/frontend/src/features/auth/useCountdown.test.tsx +33 -0
  116. package/frontend/src/features/auth/useCountdown.ts +28 -0
  117. package/frontend/src/features/idea/CreateIdeaForm.tsx +279 -0
  118. package/frontend/src/features/idea/create.ts +123 -0
  119. package/frontend/src/shared/api/activity.ts +20 -0
  120. package/frontend/src/shared/api/http.ts +2 -0
  121. package/frontend/src/shared/api/index.ts +1 -0
  122. package/frontend/src/shared/api/types.generated.ts +319 -1
  123. package/frontend/src/shared/i18n/locales/en.ts +46 -4
  124. package/frontend/src/shared/i18n/locales/zh-TW.ts +46 -4
  125. package/frontend/src/shared/slides/index.ts +74 -0
  126. package/frontend/src/shared/slides/slides.test.ts +51 -0
  127. package/frontend/src/shared/ui/copy-button.tsx +52 -0
  128. package/frontend/src/shared/ui/native-select.tsx +27 -0
  129. package/frontend/src/shared/ui/textarea.tsx +19 -0
  130. package/frontend/src/test-setup.ts +37 -2
  131. package/frontend/vite.config.ts +3 -1
  132. package/package.json +1 -1
  133. package/testdata/expected-filename.json +93 -0
  134. package/testdata/password.json +582 -0
  135. package/testdata/permission-matrix.json +71 -0
  136. package/testdata/schema/expected-filename.schema.json +32 -0
  137. package/testdata/schema/password.schema.json +59 -0
  138. package/testdata/schema/permission-matrix.schema.json +22 -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
- beforeAll(() => server.listen({ onUnhandledFrame: 'error' }));
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(() => server.close());
38
+ afterAll(() => {
39
+ globalThis.fetch = mswFetch;
40
+ server.close();
41
+ });
@@ -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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@johnnyboy11234/esg-idea-contest",
3
- "version": "0.3.0",
3
+ "version": "0.3.2",
4
4
  "description": "ESG Idea Contest registration system: Go backend, React frontend and design documents (source distribution).",
5
5
  "license": "MIT",
6
6
  "author": "Brian Huang",
@@ -0,0 +1,93 @@
1
+ {
2
+ "description": "Shared by backend (Go) and frontend (TS). Expected slide filename base per PRD 6.8 and the 255 UTF-8 byte rule of PRD 6.2 (length measured with .pptx). Inputs may be NFD; wantBase is always NFC.",
3
+ "cases": [
4
+ {
5
+ "id": "cool-idea-pdf-example",
6
+ "category": "COOL_IDEA",
7
+ "org": "BSID",
8
+ "leaderName": "\u738b\u5c0f\u660e",
9
+ "leaderEmpNo": "089290",
10
+ "ideaName": "\u6210\u7acb\u4f9b\u61c9\u5546\u6c38\u7e8c\u5b78\u9662",
11
+ "wantBase": "I_BSID_\u738b\u5c0f\u660e_089290_\u6210\u7acb\u4f9b\u61c9\u5546\u6c38\u7e8c\u5b78\u9662",
12
+ "wantBytes": 56,
13
+ "wantTooLong": false
14
+ },
15
+ {
16
+ "id": "outstanding-impact-example",
17
+ "category": "OUTSTANDING_IMPACT",
18
+ "org": "AAID",
19
+ "leaderName": "\u838a\u80b2\u6210",
20
+ "leaderEmpNo": "054616",
21
+ "ideaName": "AI\u79d8\u5bc6\u6b66\u5668\u667a\u80fd\u5c0d\u8a71",
22
+ "wantBase": "O_AAID_\u838a\u80b2\u6210_054616_AI\u79d8\u5bc6\u6b66\u5668\u667a\u80fd\u5c0d\u8a71",
23
+ "wantBytes": 55,
24
+ "wantTooLong": false
25
+ },
26
+ {
27
+ "id": "name-with-inner-space",
28
+ "category": "COOL_IDEA",
29
+ "org": "TSID",
30
+ "leaderName": "Alice Wang",
31
+ "leaderEmpNo": "000001",
32
+ "ideaName": "Green Plant 2.0",
33
+ "wantBase": "I_TSID_Alice Wang_000001_Green Plant 2.0",
34
+ "wantBytes": 45,
35
+ "wantTooLong": false
36
+ },
37
+ {
38
+ "id": "idea-name-nfd-becomes-nfc",
39
+ "category": "COOL_IDEA",
40
+ "org": "ICSD",
41
+ "leaderName": "Zoe",
42
+ "leaderEmpNo": "000002",
43
+ "ideaName": "Cafe\u0301 \u30ab\u3099",
44
+ "wantBase": "I_ICSD_Zoe_000002_Caf\u00e9 \u30ac",
45
+ "wantBytes": 32,
46
+ "wantTooLong": false
47
+ },
48
+ {
49
+ "id": "leader-name-nfd-becomes-nfc",
50
+ "category": "OUTSTANDING_IMPACT",
51
+ "org": "PLED",
52
+ "leaderName": "Jose\u0301",
53
+ "leaderEmpNo": "000003",
54
+ "ideaName": "Reuse",
55
+ "wantBase": "O_PLED_Jos\u00e9_000003_Reuse",
56
+ "wantBytes": 30,
57
+ "wantTooLong": false
58
+ },
59
+ {
60
+ "id": "exactly-255-bytes",
61
+ "category": "OUTSTANDING_IMPACT",
62
+ "org": "AAID",
63
+ "leaderName": "\u838a\u80b2\u6210",
64
+ "leaderEmpNo": "054616",
65
+ "ideaName": "\u5275\u5275\u5275\u5275\u5275\u5275\u5275\u5275\u5275\u5275\u5275\u5275\u5275\u5275\u5275\u5275\u5275\u5275\u5275\u5275\u5275\u5275\u5275\u5275\u5275\u5275\u5275\u5275\u5275\u5275\u5275\u5275\u5275\u5275\u5275\u5275\u5275\u5275\u5275\u5275\u5275\u5275\u5275\u5275\u5275\u5275\u5275\u5275\u5275\u5275\u5275\u5275\u5275\u5275\u5275\u5275\u5275\u5275\u5275\u5275\u5275\u5275\u5275\u5275\u5275\u5275\u5275\u5275\u5275\u5275\u5275\u5275\u5275\u5275abcd",
66
+ "wantBase": "O_AAID_\u838a\u80b2\u6210_054616_\u5275\u5275\u5275\u5275\u5275\u5275\u5275\u5275\u5275\u5275\u5275\u5275\u5275\u5275\u5275\u5275\u5275\u5275\u5275\u5275\u5275\u5275\u5275\u5275\u5275\u5275\u5275\u5275\u5275\u5275\u5275\u5275\u5275\u5275\u5275\u5275\u5275\u5275\u5275\u5275\u5275\u5275\u5275\u5275\u5275\u5275\u5275\u5275\u5275\u5275\u5275\u5275\u5275\u5275\u5275\u5275\u5275\u5275\u5275\u5275\u5275\u5275\u5275\u5275\u5275\u5275\u5275\u5275\u5275\u5275\u5275\u5275\u5275\u5275abcd",
67
+ "wantBytes": 255,
68
+ "wantTooLong": false
69
+ },
70
+ {
71
+ "id": "256-bytes-too-long",
72
+ "category": "OUTSTANDING_IMPACT",
73
+ "org": "AAID",
74
+ "leaderName": "\u838a\u80b2\u6210",
75
+ "leaderEmpNo": "054616",
76
+ "ideaName": "\u5275\u5275\u5275\u5275\u5275\u5275\u5275\u5275\u5275\u5275\u5275\u5275\u5275\u5275\u5275\u5275\u5275\u5275\u5275\u5275\u5275\u5275\u5275\u5275\u5275\u5275\u5275\u5275\u5275\u5275\u5275\u5275\u5275\u5275\u5275\u5275\u5275\u5275\u5275\u5275\u5275\u5275\u5275\u5275\u5275\u5275\u5275\u5275\u5275\u5275\u5275\u5275\u5275\u5275\u5275\u5275\u5275\u5275\u5275\u5275\u5275\u5275\u5275\u5275\u5275\u5275\u5275\u5275\u5275\u5275\u5275\u5275\u5275\u5275abcde",
77
+ "wantBase": "O_AAID_\u838a\u80b2\u6210_054616_\u5275\u5275\u5275\u5275\u5275\u5275\u5275\u5275\u5275\u5275\u5275\u5275\u5275\u5275\u5275\u5275\u5275\u5275\u5275\u5275\u5275\u5275\u5275\u5275\u5275\u5275\u5275\u5275\u5275\u5275\u5275\u5275\u5275\u5275\u5275\u5275\u5275\u5275\u5275\u5275\u5275\u5275\u5275\u5275\u5275\u5275\u5275\u5275\u5275\u5275\u5275\u5275\u5275\u5275\u5275\u5275\u5275\u5275\u5275\u5275\u5275\u5275\u5275\u5275\u5275\u5275\u5275\u5275\u5275\u5275\u5275\u5275\u5275\u5275abcde",
78
+ "wantBytes": 256,
79
+ "wantTooLong": true
80
+ },
81
+ {
82
+ "id": "emoji-four-byte-too-long",
83
+ "category": "COOL_IDEA",
84
+ "org": "DEID",
85
+ "leaderName": "\u738b\u5c0f\u660e",
86
+ "leaderEmpNo": "089290",
87
+ "ideaName": "\ud83d\ude00\ud83d\ude00\ud83d\ude00\ud83d\ude00\ud83d\ude00\ud83d\ude00\ud83d\ude00\ud83d\ude00\ud83d\ude00\ud83d\ude00\ud83d\ude00\ud83d\ude00\ud83d\ude00\ud83d\ude00\ud83d\ude00\ud83d\ude00\ud83d\ude00\ud83d\ude00\ud83d\ude00\ud83d\ude00\ud83d\ude00\ud83d\ude00\ud83d\ude00\ud83d\ude00\ud83d\ude00\ud83d\ude00\ud83d\ude00\ud83d\ude00\ud83d\ude00\ud83d\ude00\ud83d\ude00\ud83d\ude00\ud83d\ude00\ud83d\ude00\ud83d\ude00\ud83d\ude00\ud83d\ude00\ud83d\ude00\ud83d\ude00\ud83d\ude00\ud83d\ude00\ud83d\ude00\ud83d\ude00\ud83d\ude00\ud83d\ude00\ud83d\ude00\ud83d\ude00\ud83d\ude00\ud83d\ude00\ud83d\ude00\ud83d\ude00\ud83d\ude00\ud83d\ude00\ud83d\ude00\ud83d\ude00\ud83d\ude00\ud83d\ude00\ud83d\ude00\ud83d\ude00\ud83d\ude00",
88
+ "wantBase": "I_DEID_\u738b\u5c0f\u660e_089290_\ud83d\ude00\ud83d\ude00\ud83d\ude00\ud83d\ude00\ud83d\ude00\ud83d\ude00\ud83d\ude00\ud83d\ude00\ud83d\ude00\ud83d\ude00\ud83d\ude00\ud83d\ude00\ud83d\ude00\ud83d\ude00\ud83d\ude00\ud83d\ude00\ud83d\ude00\ud83d\ude00\ud83d\ude00\ud83d\ude00\ud83d\ude00\ud83d\ude00\ud83d\ude00\ud83d\ude00\ud83d\ude00\ud83d\ude00\ud83d\ude00\ud83d\ude00\ud83d\ude00\ud83d\ude00\ud83d\ude00\ud83d\ude00\ud83d\ude00\ud83d\ude00\ud83d\ude00\ud83d\ude00\ud83d\ude00\ud83d\ude00\ud83d\ude00\ud83d\ude00\ud83d\ude00\ud83d\ude00\ud83d\ude00\ud83d\ude00\ud83d\ude00\ud83d\ude00\ud83d\ude00\ud83d\ude00\ud83d\ude00\ud83d\ude00\ud83d\ude00\ud83d\ude00\ud83d\ude00\ud83d\ude00\ud83d\ude00\ud83d\ude00\ud83d\ude00\ud83d\ude00\ud83d\ude00\ud83d\ude00",
89
+ "wantBytes": 269,
90
+ "wantTooLong": true
91
+ }
92
+ ]
93
+ }