@johnnyboy11234/esg-idea-contest 0.2.4 → 0.3.1

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 (111) hide show
  1. package/backend/api/openapi.yaml +83 -0
  2. package/backend/cmd/server/main.go +35 -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/keyboard.go +75 -0
  13. package/backend/internal/account/keyboard_test.go +43 -0
  14. package/backend/internal/account/logging_integration_test.go +1 -1
  15. package/backend/internal/account/password.go +125 -0
  16. package/backend/internal/account/validate.go +4 -21
  17. package/backend/internal/api/api.gen.go +224 -0
  18. package/backend/internal/auth/internal/store/login_attempt.go +53 -0
  19. package/backend/internal/auth/internal/store/login_lock.go +77 -0
  20. package/backend/internal/auth/internal/store/login_throttle_audit.go +42 -0
  21. package/backend/internal/auth/internal/store/session.go +15 -7
  22. package/backend/internal/auth/login.go +16 -4
  23. package/backend/internal/auth/login_integration_test.go +14 -1
  24. package/backend/internal/auth/login_ip_integration_test.go +153 -0
  25. package/backend/internal/auth/login_lock_integration_test.go +205 -0
  26. package/backend/internal/auth/login_test.go +15 -0
  27. package/backend/internal/auth/login_throttle_audit_integration_test.go +88 -0
  28. package/backend/internal/auth/purge.go +93 -0
  29. package/backend/internal/auth/purge_integration_test.go +220 -0
  30. package/backend/internal/auth/purge_test.go +51 -0
  31. package/backend/internal/auth/session.go +0 -6
  32. package/backend/internal/auth/session_integration_test.go +3 -3
  33. package/backend/internal/auth/throttle.go +122 -0
  34. package/backend/internal/deadline/gate.go +80 -0
  35. package/backend/internal/deadline/gate_integration_test.go +67 -0
  36. package/backend/internal/deadline/gate_test.go +61 -0
  37. package/backend/internal/idea/create.go +352 -0
  38. package/backend/internal/idea/create_integration_test.go +650 -0
  39. package/backend/internal/idea/handler.go +148 -0
  40. package/backend/internal/idea/handler_integration_test.go +107 -0
  41. package/backend/internal/idea/integration_test.go +149 -0
  42. package/backend/internal/idea/internal/store/list.go +146 -0
  43. package/backend/internal/idea/internal/store/list_test.go +33 -0
  44. package/backend/internal/idea/internal/store/store.go +166 -0
  45. package/backend/internal/idea/list.go +166 -0
  46. package/backend/internal/idea/list_integration_test.go +252 -0
  47. package/backend/internal/idea/list_test.go +96 -0
  48. package/backend/internal/idea/service.go +53 -0
  49. package/backend/internal/server/api.go +17 -0
  50. package/backend/internal/server/public.go +1 -0
  51. package/backend/internal/server/slides.go +21 -0
  52. package/backend/internal/shared/config/config.go +44 -1
  53. package/backend/internal/shared/config/config_test.go +21 -13
  54. package/backend/internal/shared/db/intx_integration_test.go +179 -0
  55. package/backend/internal/shared/dbtest/dbtest.go +21 -3
  56. package/backend/internal/shared/dbtest/faults.go +162 -0
  57. package/backend/internal/shared/job/job.go +54 -0
  58. package/backend/internal/shared/job/job_test.go +105 -0
  59. package/backend/internal/shared/web/multipart.go +175 -0
  60. package/backend/internal/shared/web/multipart_test.go +169 -0
  61. package/backend/internal/shared/web/problem.go +1 -0
  62. package/backend/internal/shared/web/request.go +42 -1
  63. package/backend/internal/shared/web/web_test.go +36 -0
  64. package/backend/internal/storage/expectedfilename.go +43 -0
  65. package/backend/internal/storage/expectedfilename_test.go +16 -0
  66. package/backend/internal/storage/internal/store/pending.go +41 -0
  67. package/backend/internal/storage/minio.go +139 -0
  68. package/backend/internal/storage/minio_integration_test.go +87 -0
  69. package/backend/internal/storage/minio_test.go +84 -0
  70. package/backend/internal/storage/pending.go +144 -0
  71. package/backend/internal/storage/pending_integration_test.go +324 -0
  72. package/backend/internal/storage/pending_test.go +23 -0
  73. package/backend/internal/storage/validator.go +100 -0
  74. package/backend/internal/storage/validator_test.go +95 -0
  75. package/backend/test/contract/expected_filename_test.go +60 -0
  76. package/backend/test/contract/password_test.go +46 -0
  77. package/backend/test/matrix/file_test.go +52 -0
  78. package/backend/test/matrix/load_test.go +62 -0
  79. package/backend/test/matrix/matrix_integration_test.go +291 -0
  80. package/backend/test/migration/pending_file_deletion_test.go +46 -0
  81. package/docs/API/api-endpoints.md +1 -1
  82. package/docs/API/error-codes.md +1 -1
  83. package/frontend/public/auth-hero-login.jpg +0 -0
  84. package/frontend/public/auth-hero-register.jpg +0 -0
  85. package/frontend/src/app/App.test.tsx +2 -42
  86. package/frontend/src/app/App.tsx +11 -2
  87. package/frontend/src/app/index.css +4 -4
  88. package/frontend/src/app/layouts.tsx +7 -135
  89. package/frontend/src/app/pages/JoinRequests.tsx +2 -2
  90. package/frontend/src/app/pages/NotFound.tsx +1 -1
  91. package/frontend/src/app/pages/admin.tsx +1 -1
  92. package/frontend/src/app/pages/auth.tsx +53 -25
  93. package/frontend/src/app/pages/ideas.tsx +7 -7
  94. package/frontend/src/app/pages/login.test.tsx +3 -54
  95. package/frontend/src/app/pages/register.test.tsx +1 -1
  96. package/frontend/src/app/router.tsx +2 -6
  97. package/frontend/src/features/auth/IdleWarning.test.tsx +82 -0
  98. package/frontend/src/features/auth/IdleWarning.tsx +66 -0
  99. package/frontend/src/features/auth/LoginForm.tsx +8 -1
  100. package/frontend/src/shared/api/activity.ts +20 -0
  101. package/frontend/src/shared/api/http.ts +2 -0
  102. package/frontend/src/shared/api/index.ts +1 -0
  103. package/frontend/src/shared/i18n/locales/en.ts +1 -0
  104. package/frontend/src/shared/i18n/locales/zh-TW.ts +1 -0
  105. package/package.json +1 -1
  106. package/testdata/expected-filename.json +93 -0
  107. package/testdata/password.json +425 -0
  108. package/testdata/permission-matrix.json +157 -0
  109. package/testdata/schema/expected-filename.schema.json +32 -0
  110. package/testdata/schema/password.schema.json +59 -0
  111. package/testdata/schema/permission-matrix.schema.json +73 -0
@@ -0,0 +1,20 @@
1
+ /**
2
+ * Time of the last answered server request (Frontend.md §8.1, OI-22). Any answer except a 401 means
3
+ * the server saw the session, so the idle clock restarts; the idle-timeout warning reads it.
4
+ */
5
+ let last = Date.now();
6
+ const listeners = new Set<() => void>();
7
+
8
+ export const lastActivity = () => last;
9
+
10
+ export function markActivity(): void {
11
+ last = Date.now();
12
+ listeners.forEach((fn) => fn());
13
+ }
14
+
15
+ export function subscribeActivity(fn: () => void): () => void {
16
+ listeners.add(fn);
17
+ return () => {
18
+ listeners.delete(fn);
19
+ };
20
+ }
@@ -1,3 +1,4 @@
1
+ import { markActivity } from './activity';
1
2
  import { API_BASE, clearCsrfToken, getCsrfToken, refreshCsrfToken } from './csrf';
2
3
  import { parseApiError, type ApiError } from './problem';
3
4
 
@@ -73,6 +74,7 @@ export async function apiFetch(path: string, init: ApiRequestInit = {}): Promise
73
74
  res = await send(await refreshCsrfToken());
74
75
  }
75
76
 
77
+ if (res.status !== 401) markActivity();
76
78
  if (res.ok) {
77
79
  if (write && SESSION_CHANGING.has(path)) clearCsrfToken();
78
80
  return res;
@@ -4,3 +4,4 @@ export { apiFetch, setUnauthenticatedHandler } from './http';
4
4
  export type { ApiRequestInit } from './http';
5
5
  export { clearCsrfToken, getCsrfToken } from './csrf';
6
6
  export type { paths, components } from './types.generated';
7
+ export { lastActivity, markActivity, subscribeActivity } from './activity';
@@ -57,6 +57,7 @@ export const en = {
57
57
  leaderEmpNo: 'Leader employee ID',
58
58
  },
59
59
  pages: {
60
+ idle: { title: 'Are you still there?', body: 'You have been inactive for a while. For your security your session will end in about 2 minutes unless you continue.', continue: 'Continue' },
60
61
  login: { title: 'Sign in to your account', description: 'Enter your employee ID and password.', noAccount: 'No account yet?', panelTitle: 'ESG Idea Contest', panelBody: 'Register, form a team and submit your Idea.', submitting: 'Signing in…', sessionEnded: 'Your session has ended. Please sign in again.' },
61
62
  register: {
62
63
  title: 'Create an account',
@@ -54,6 +54,7 @@ export const zhTW: Dictionary = {
54
54
  leaderEmpNo: '組長員工編號',
55
55
  },
56
56
  pages: {
57
+ idle: { title: '您還在嗎?', body: '您已閒置一段時間。為了安全,若不繼續使用,您的登入將在約 2 分鐘後結束。', continue: '繼續' },
57
58
  login: { title: '登入您的帳號', description: '請輸入員工編號與密碼。', noAccount: '還沒有帳號?', panelTitle: 'ESG 創意競賽', panelBody: '註冊、組隊並提交您的創意。', submitting: '登入中…', sessionEnded: '您的登入已結束,請重新登入。' },
58
59
  register: {
59
60
  title: '建立帳號',
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@johnnyboy11234/esg-idea-contest",
3
- "version": "0.2.4",
3
+ "version": "0.3.1",
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
+ }
@@ -0,0 +1,425 @@
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). Rules not implemented yet (SEQUENCE, COMMON_PASSWORD: B1b-03 and later) have no cases here; those cases are added with their rules, and these inputs avoid triggering them. PRD 6.4.",
3
+ "cases": [
4
+ {
5
+ "id": "valid-typical",
6
+ "password": "Correct-Horse-1",
7
+ "empNo": "089290",
8
+ "wantRules": []
9
+ },
10
+ {
11
+ "id": "valid-min-length-12",
12
+ "password": "Abcdefgh1!xY",
13
+ "empNo": "",
14
+ "wantRules": []
15
+ },
16
+ {
17
+ "id": "valid-max-length-64",
18
+ "password": "Ab1!Ab1!Ab1!Ab1!Ab1!Ab1!Ab1!Ab1!Ab1!Ab1!Ab1!Ab1!Ab1!Ab1!Ab1!Ab1!",
19
+ "empNo": "",
20
+ "wantRules": []
21
+ },
22
+ {
23
+ "id": "valid-space-is-symbol",
24
+ "password": "Correct Horse1",
25
+ "empNo": "",
26
+ "wantRules": []
27
+ },
28
+ {
29
+ "id": "valid-three-classes-no-symbol",
30
+ "password": "CorrectHorse12",
31
+ "empNo": "",
32
+ "wantRules": []
33
+ },
34
+ {
35
+ "id": "valid-three-classes-no-digit",
36
+ "password": "Correct-Horse-x",
37
+ "empNo": "",
38
+ "wantRules": []
39
+ },
40
+ {
41
+ "id": "valid-three-classes-no-upper",
42
+ "password": "correct-horse-1",
43
+ "empNo": "",
44
+ "wantRules": []
45
+ },
46
+ {
47
+ "id": "valid-three-classes-no-lower",
48
+ "password": "CORRECT-HORSE-1",
49
+ "empNo": "",
50
+ "wantRules": []
51
+ },
52
+ {
53
+ "id": "length-11",
54
+ "password": "Abcdefgh1!x",
55
+ "empNo": "",
56
+ "wantRules": [
57
+ "LENGTH"
58
+ ]
59
+ },
60
+ {
61
+ "id": "length-65",
62
+ "password": "Ab1!Ab1!Ab1!Ab1!Ab1!Ab1!Ab1!Ab1!Ab1!Ab1!Ab1!Ab1!Ab1!Ab1!Ab1!Ab1!x",
63
+ "empNo": "",
64
+ "wantRules": [
65
+ "LENGTH"
66
+ ]
67
+ },
68
+ {
69
+ "id": "length-empty",
70
+ "password": "",
71
+ "empNo": "",
72
+ "wantRules": [
73
+ "LENGTH",
74
+ "COMPLEXITY"
75
+ ]
76
+ },
77
+ {
78
+ "id": "length-counts-code-points-not-bytes",
79
+ "password": "Aa1!Aa1!Aa1é",
80
+ "empNo": "",
81
+ "wantRules": [
82
+ "CHARSET"
83
+ ]
84
+ },
85
+ {
86
+ "id": "charset-non-ascii",
87
+ "password": "Correct-Horse-1é",
88
+ "empNo": "",
89
+ "wantRules": [
90
+ "CHARSET"
91
+ ]
92
+ },
93
+ {
94
+ "id": "charset-cjk-only-12",
95
+ "password": "密碼密碼密碼密碼密碼密碼",
96
+ "empNo": "",
97
+ "wantRules": [
98
+ "CHARSET",
99
+ "COMPLEXITY"
100
+ ]
101
+ },
102
+ {
103
+ "id": "charset-tab",
104
+ "password": "Correct\tHorse-1",
105
+ "empNo": "",
106
+ "wantRules": [
107
+ "CHARSET"
108
+ ]
109
+ },
110
+ {
111
+ "id": "charset-del",
112
+ "password": "Correct-Horse-1",
113
+ "empNo": "",
114
+ "wantRules": [
115
+ "CHARSET"
116
+ ]
117
+ },
118
+ {
119
+ "id": "charset-emoji-counts-as-one-code-point",
120
+ "password": "Correct-Hor1😀",
121
+ "empNo": "",
122
+ "wantRules": [
123
+ "CHARSET"
124
+ ]
125
+ },
126
+ {
127
+ "id": "complexity-two-classes-lower-digit",
128
+ "password": "abcdefghij12",
129
+ "empNo": "",
130
+ "wantRules": [
131
+ "COMPLEXITY"
132
+ ]
133
+ },
134
+ {
135
+ "id": "complexity-two-classes-lower-symbol",
136
+ "password": "whatever-goes",
137
+ "empNo": "",
138
+ "wantRules": [
139
+ "COMPLEXITY"
140
+ ]
141
+ },
142
+ {
143
+ "id": "complexity-two-classes-upper-lower",
144
+ "password": "CorrectHorseXy",
145
+ "empNo": "",
146
+ "wantRules": [
147
+ "COMPLEXITY"
148
+ ]
149
+ },
150
+ {
151
+ "id": "complexity-non-ascii-is-no-class",
152
+ "password": "abcdefghijk1é",
153
+ "empNo": "",
154
+ "wantRules": [
155
+ "CHARSET",
156
+ "COMPLEXITY"
157
+ ]
158
+ },
159
+ {
160
+ "id": "repeat-three-lower",
161
+ "password": "Correct-Hoooorse1",
162
+ "empNo": "",
163
+ "wantRules": [
164
+ "REPEAT"
165
+ ]
166
+ },
167
+ {
168
+ "id": "repeat-three-digit",
169
+ "password": "Correct-Horse111",
170
+ "empNo": "",
171
+ "wantRules": [
172
+ "REPEAT"
173
+ ]
174
+ },
175
+ {
176
+ "id": "repeat-exactly-two-ok",
177
+ "password": "Correct-Hoorse-11",
178
+ "empNo": "",
179
+ "wantRules": []
180
+ },
181
+ {
182
+ "id": "repeat-case-sensitive",
183
+ "password": "Correct-HoOorse-1",
184
+ "empNo": "",
185
+ "wantRules": []
186
+ },
187
+ {
188
+ "id": "repeat-symbols",
189
+ "password": "Correct-Horse1!!!",
190
+ "empNo": "",
191
+ "wantRules": [
192
+ "REPEAT"
193
+ ]
194
+ },
195
+ {
196
+ "id": "emp-no-contained",
197
+ "password": "Correct-089290-Ab",
198
+ "empNo": "089290",
199
+ "wantRules": [
200
+ "CONTAINS_EMP_NO"
201
+ ]
202
+ },
203
+ {
204
+ "id": "emp-no-contained-at-start",
205
+ "password": "089290-Correct-Ab",
206
+ "empNo": "089290",
207
+ "wantRules": [
208
+ "CONTAINS_EMP_NO"
209
+ ]
210
+ },
211
+ {
212
+ "id": "emp-no-skipped-when-malformed",
213
+ "password": "Correct-089290-Ab",
214
+ "empNo": "",
215
+ "wantRules": []
216
+ },
217
+ {
218
+ "id": "emp-no-other-number-ok",
219
+ "password": "Correct-089291-Ab",
220
+ "empNo": "089290",
221
+ "wantRules": []
222
+ },
223
+ {
224
+ "id": "table-order-all-violated",
225
+ "password": "aaaé",
226
+ "empNo": "123456",
227
+ "wantRules": [
228
+ "LENGTH",
229
+ "CHARSET",
230
+ "COMPLEXITY",
231
+ "REPEAT"
232
+ ]
233
+ },
234
+ {
235
+ "id": "table-order-with-emp-no",
236
+ "password": "aaa123456",
237
+ "empNo": "123456",
238
+ "wantRules": [
239
+ "LENGTH",
240
+ "COMPLEXITY",
241
+ "KEYBOARD_RUN",
242
+ "REPEAT",
243
+ "CONTAINS_EMP_NO"
244
+ ]
245
+ },
246
+ {
247
+ "id": "table-order-full-set",
248
+ "password": "aaaaaaaaaaaaé123456",
249
+ "empNo": "123456",
250
+ "wantRules": [
251
+ "CHARSET",
252
+ "COMPLEXITY",
253
+ "KEYBOARD_RUN",
254
+ "REPEAT",
255
+ "CONTAINS_EMP_NO"
256
+ ]
257
+ },
258
+ {
259
+ "id": "keyboard-row-digits",
260
+ "password": "Xk9-1234-Pmzj",
261
+ "empNo": "",
262
+ "wantRules": [
263
+ "KEYBOARD_RUN"
264
+ ]
265
+ },
266
+ {
267
+ "id": "keyboard-row-qwer",
268
+ "password": "Zq-qwer-Mj81",
269
+ "empNo": "",
270
+ "wantRules": [
271
+ "KEYBOARD_RUN"
272
+ ]
273
+ },
274
+ {
275
+ "id": "keyboard-row-asdf",
276
+ "password": "Zq-Asdf-Mj81",
277
+ "empNo": "",
278
+ "wantRules": [
279
+ "KEYBOARD_RUN"
280
+ ]
281
+ },
282
+ {
283
+ "id": "keyboard-row-bottom",
284
+ "password": "Zq-xcvb-Mj81",
285
+ "empNo": "",
286
+ "wantRules": [
287
+ "KEYBOARD_RUN"
288
+ ]
289
+ },
290
+ {
291
+ "id": "keyboard-reverse",
292
+ "password": "Zq-rewq-Mj81",
293
+ "empNo": "",
294
+ "wantRules": [
295
+ "KEYBOARD_RUN"
296
+ ]
297
+ },
298
+ {
299
+ "id": "keyboard-reverse-digits",
300
+ "password": "Zq-4321-Mj8P",
301
+ "empNo": "",
302
+ "wantRules": [
303
+ "KEYBOARD_RUN"
304
+ ]
305
+ },
306
+ {
307
+ "id": "keyboard-case-insensitive",
308
+ "password": "Zq-qWeR-Mj81",
309
+ "empNo": "",
310
+ "wantRules": [
311
+ "KEYBOARD_RUN"
312
+ ]
313
+ },
314
+ {
315
+ "id": "keyboard-shifted-symbols",
316
+ "password": "Zq-!@#$-Mjkp",
317
+ "empNo": "",
318
+ "wantRules": [
319
+ "KEYBOARD_RUN"
320
+ ]
321
+ },
322
+ {
323
+ "id": "keyboard-mixed-shift-and-plain",
324
+ "password": "Zq-1@3$-Mjkp",
325
+ "empNo": "",
326
+ "wantRules": [
327
+ "KEYBOARD_RUN"
328
+ ]
329
+ },
330
+ {
331
+ "id": "keyboard-longer-run-reported-once",
332
+ "password": "Zq-qwertyu-M1",
333
+ "empNo": "",
334
+ "wantRules": [
335
+ "KEYBOARD_RUN"
336
+ ]
337
+ },
338
+ {
339
+ "id": "keyboard-run-at-start",
340
+ "password": "qwerZk-Mj81xP",
341
+ "empNo": "",
342
+ "wantRules": [
343
+ "KEYBOARD_RUN"
344
+ ]
345
+ },
346
+ {
347
+ "id": "keyboard-run-at-end",
348
+ "password": "Zk-Mj81xP-poiu",
349
+ "empNo": "",
350
+ "wantRules": [
351
+ "KEYBOARD_RUN"
352
+ ]
353
+ },
354
+ {
355
+ "id": "keyboard-punctuation-row",
356
+ "password": "Zk-Mj81xP;lkj",
357
+ "empNo": "",
358
+ "wantRules": [
359
+ "KEYBOARD_RUN"
360
+ ]
361
+ },
362
+ {
363
+ "id": "keyboard-three-keys-ok",
364
+ "password": "Zq-qwe-Mj81xP",
365
+ "empNo": "",
366
+ "wantRules": []
367
+ },
368
+ {
369
+ "id": "keyboard-row-end-does-not-wrap-to-next-row",
370
+ "password": "Zq-90-qw-Mj81",
371
+ "empNo": "",
372
+ "wantRules": []
373
+ },
374
+ {
375
+ "id": "keyboard-rows-do-not-join",
376
+ "password": "Zq-pasdMj81xP",
377
+ "empNo": "",
378
+ "wantRules": []
379
+ },
380
+ {
381
+ "id": "keyboard-direction-change-ok",
382
+ "password": "Zq-qwewq-Mj81",
383
+ "empNo": "",
384
+ "wantRules": []
385
+ },
386
+ {
387
+ "id": "keyboard-gap-ok",
388
+ "password": "Zq-qwdf-Mj81xP",
389
+ "empNo": "",
390
+ "wantRules": []
391
+ },
392
+ {
393
+ "id": "keyboard-non-row-glyph-breaks-run",
394
+ "password": "Zq-qw~er-Mj81",
395
+ "empNo": "",
396
+ "wantRules": []
397
+ },
398
+ {
399
+ "id": "keyboard-repeat-is-not-a-run",
400
+ "password": "Zq-qqqq-Mj81xP",
401
+ "empNo": "",
402
+ "wantRules": [
403
+ "REPEAT"
404
+ ]
405
+ },
406
+ {
407
+ "id": "keyboard-and-complexity",
408
+ "password": "qwertyuiopas",
409
+ "empNo": "",
410
+ "wantRules": [
411
+ "COMPLEXITY",
412
+ "KEYBOARD_RUN"
413
+ ]
414
+ },
415
+ {
416
+ "id": "keyboard-before-repeat-in-table-order",
417
+ "password": "Zq-1234-aaa-Mj",
418
+ "empNo": "",
419
+ "wantRules": [
420
+ "KEYBOARD_RUN",
421
+ "REPEAT"
422
+ ]
423
+ }
424
+ ]
425
+ }