@sungen/driver-data-factory 3.2.10-beta.15 → 3.2.10-beta.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/common/field-types/currency.yaml +10 -10
- package/common/field-types/email.yaml +16 -16
- package/common/field-types/jp/katakana.yaml +12 -12
- package/common/field-types/number-float.yaml +10 -10
- package/common/field-types/number-integer.yaml +12 -12
- package/common/field-types/password.yaml +22 -22
- package/common/field-types/phone.yaml +14 -14
- package/common/field-types/postcode.yaml +8 -8
- package/common/field-types/string.yaml +11 -11
- package/common/security/broken-auth.yaml +6 -6
- package/common/security/idor.yaml +5 -5
- package/common/security/input-validation.yaml +5 -5
- package/common/security/sqli.yaml +7 -7
- package/common/security/upload.yaml +7 -7
- package/common/security/xss.yaml +9 -9
- package/common/techniques/boundary.yaml +4 -4
- package/common/techniques/decision-table.yaml +4 -4
- package/common/techniques/equivalence.yaml +4 -4
- package/common/techniques/state-transition.yaml +4 -4
- package/package.json +2 -2
- package/src/catalog-loader.ts +2 -2
- package/src/cli.ts +5 -54
- package/src/generator.ts +25 -86
- package/src/index.ts +0 -2
- package/src/lint.ts +13 -23
- package/src/types.ts +1 -1
- package/src/fixture.ts +0 -79
- package/src/readme.ts +0 -52
|
@@ -1,20 +1,20 @@
|
|
|
1
1
|
# Field-type: currency (money) — from common-checklist.xlsx (Input box · Currency).
|
|
2
2
|
type: currency
|
|
3
|
-
description:
|
|
3
|
+
description: Số tiền — số nguyên/thập phân theo đơn vị tiền tệ; lưu ý leading-zero & giá trị 0.
|
|
4
4
|
constraints:
|
|
5
5
|
min: 0
|
|
6
6
|
max: 999999999 # placeholder
|
|
7
|
-
decimalPlaces: 0 # placeholder (JPY=0, USD=2
|
|
7
|
+
decimalPlaces: 0 # placeholder (JPY=0, USD=2 …)
|
|
8
8
|
valid:
|
|
9
|
-
- { id: CHK-CUR-01, value: "1000", note: "
|
|
10
|
-
- { id: CHK-CUR-02, value: "0", note: "0
|
|
9
|
+
- { id: CHK-CUR-01, value: "1000", note: "số tiền hợp lệ" }
|
|
10
|
+
- { id: CHK-CUR-02, value: "0", note: "giá trị 0 phải hiểu là 0, KHÔNG phải 'chưa nhập'" }
|
|
11
11
|
boundary:
|
|
12
|
-
- { id: CHK-CUR-B1, gen: "
|
|
13
|
-
- { id: CHK-CUR-B2, gen: "
|
|
12
|
+
- { id: CHK-CUR-B1, gen: "Min / Min+1" }
|
|
13
|
+
- { id: CHK-CUR-B2, gen: "Max / Max-1" }
|
|
14
14
|
invalid:
|
|
15
|
-
- { id: CHK-CUR-I1, gen: "
|
|
16
|
-
- { id: CHK-CUR-I2, gen: "
|
|
17
|
-
- { id: CHK-CUR-I3, value: "", reason: "
|
|
15
|
+
- { id: CHK-CUR-I1, gen: "chữ cái / ký tự lạ", reason: "không phải số", errorRef: M13 }
|
|
16
|
+
- { id: CHK-CUR-I2, gen: "Min-1 / Max+1", reason: "vượt biên", errorRef: M13 }
|
|
17
|
+
- { id: CHK-CUR-I3, value: "", reason: "rỗng (tuỳ spec)", errorRef: M02 }
|
|
18
18
|
notes:
|
|
19
|
-
- "
|
|
19
|
+
- "Kiểm tra riêng: leading zero (0100) có bị cắt mất không; format hiển thị dấu phân cách nghìn."
|
|
20
20
|
uses_security: [sqli]
|
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
# Field-type: email — seeded from common-checklist.xlsx (Input box · Email) +
|
|
1
|
+
# Field-type: email — seeded from common-checklist.xlsx (Input box · Email) + phuong-phap §Nguồn-2.
|
|
2
2
|
# errorRef (Mxx) are PLACEHOLDERS — the generator maps them onto the project's real message codes;
|
|
3
|
-
# if the project spec has none, the value is marked as
|
|
3
|
+
# if the project spec has none, the value is marked as a chua-chot (open) point, not invented.
|
|
4
4
|
type: email
|
|
5
|
-
description:
|
|
5
|
+
description: Email half-width, format <local-part>@<domain>
|
|
6
6
|
format: "<local-part>@<domain>"
|
|
7
7
|
constraints:
|
|
8
8
|
charset: half-width
|
|
@@ -13,18 +13,18 @@ valid:
|
|
|
13
13
|
- { id: CHK-EMAIL-01, value: "user@example.com", note: "normal half-width" }
|
|
14
14
|
- { id: CHK-EMAIL-02, value: "u.name+tag@sub.example.co.jp", note: "dotted local + subdomain" }
|
|
15
15
|
boundary:
|
|
16
|
-
- { id: CHK-EMAIL-B1, gen: "local
|
|
17
|
-
- { id: CHK-EMAIL-B2, gen: "local
|
|
18
|
-
- { id: CHK-EMAIL-B3, gen: "
|
|
16
|
+
- { id: CHK-EMAIL-B1, gen: "local@ đúng maxLocal", note: "local part = max (64)" }
|
|
17
|
+
- { id: CHK-EMAIL-B2, gen: "local@ maxLocal-1", note: "local part = max-1" }
|
|
18
|
+
- { id: CHK-EMAIL-B3, gen: "chuỗi tổng = maxTotal", note: "total length = max (255)" }
|
|
19
19
|
invalid:
|
|
20
|
-
- { id: CHK-EMAIL-I1, value: "user@", reason: "
|
|
21
|
-
- { id: CHK-EMAIL-I2, value: "user@.com", reason: "
|
|
22
|
-
- { id: CHK-EMAIL-I3, value: "u@@b.com", reason: "
|
|
23
|
-
- { id: CHK-EMAIL-I4, value: "@example.com", reason: "
|
|
24
|
-
- { id: CHK-EMAIL-I5, value: "user@example.com", reason: "full-width
|
|
25
|
-
- { id: CHK-EMAIL-I6, gen: "local >
|
|
26
|
-
- { id: CHK-EMAIL-I7, gen: "
|
|
27
|
-
- { id: CHK-EMAIL-I8, value: "", reason: "
|
|
20
|
+
- { id: CHK-EMAIL-I1, value: "user@", reason: "thiếu domain", errorRef: M13 }
|
|
21
|
+
- { id: CHK-EMAIL-I2, value: "user@.com", reason: "dấu . sai vị trí", errorRef: M13 }
|
|
22
|
+
- { id: CHK-EMAIL-I3, value: "u@@b.com", reason: "hai dấu @", errorRef: M13 }
|
|
23
|
+
- { id: CHK-EMAIL-I4, value: "@example.com", reason: "thiếu local-part", errorRef: M13 }
|
|
24
|
+
- { id: CHK-EMAIL-I5, value: "user@example.com", reason: "full-width character", errorRef: M39 }
|
|
25
|
+
- { id: CHK-EMAIL-I6, gen: "local > 64 ký tự", reason: "vượt maxLocal", errorRef: M13 }
|
|
26
|
+
- { id: CHK-EMAIL-I7, gen: "tổng > 255 ký tự", reason: "vượt maxTotal", errorRef: M13 }
|
|
27
|
+
- { id: CHK-EMAIL-I8, value: "", reason: "rỗng (required)", errorRef: M02 }
|
|
28
28
|
notes:
|
|
29
|
-
- "
|
|
30
|
-
- "Soft-delete:
|
|
29
|
+
- "Chuẩn hoá: lowercase, NFKC, trim trước khi so khớp."
|
|
30
|
+
- "Soft-delete: nếu hệ thống gắn hậu tố vào email đã xoá mềm → test maxTotal THẤP hơn giới hạn cột DB để chừa chỗ hậu tố."
|
|
@@ -1,20 +1,20 @@
|
|
|
1
|
-
# Field-type: Katakana name (JP-specific) — from
|
|
1
|
+
# Field-type: Katakana name (JP-specific) — from phuong-phap §Nguồn-2 (kinh nghiệm đặc thù JP).
|
|
2
2
|
type: katakana
|
|
3
|
-
description: Katakana
|
|
3
|
+
description: Trường chỉ nhận Katakana (họ/tên kana trong hệ thống Nhật).
|
|
4
4
|
constraints:
|
|
5
5
|
charset: "full-width Katakana only"
|
|
6
6
|
valid:
|
|
7
|
-
- { id: CHK-KANA-01, value: "ヤマダ", note: "
|
|
8
|
-
- { id: CHK-KANA-02, value: "ジョウジ", note: "
|
|
7
|
+
- { id: CHK-KANA-01, value: "ヤマダ", note: "Katakana chuẩn" }
|
|
8
|
+
- { id: CHK-KANA-02, value: "ジョウジ", note: "có dakuten + trường âm hợp lệ" }
|
|
9
9
|
boundary:
|
|
10
|
-
- { id: CHK-KANA-B1, gen: "min / max
|
|
10
|
+
- { id: CHK-KANA-B1, gen: "đúng min / max độ dài" }
|
|
11
11
|
invalid:
|
|
12
|
-
- { id: CHK-KANA-I1, value: "yamada", reason: "
|
|
13
|
-
- { id: CHK-KANA-I2, value: "やまだ", reason: "Hiragana
|
|
14
|
-
- { id: CHK-KANA-I3, value: "山田", reason: "Kanji
|
|
15
|
-
- { id: CHK-KANA-I4, value: "ヤマダ", reason: "half-width kana (
|
|
16
|
-
- { id: CHK-KANA-I5, value: "ゝ々", reason: "
|
|
17
|
-
- { id: CHK-KANA-I6, value: "ャュョ", reason: "
|
|
12
|
+
- { id: CHK-KANA-I1, value: "yamada", reason: "lẫn ký tự Latin", errorRef: M39 }
|
|
13
|
+
- { id: CHK-KANA-I2, value: "やまだ", reason: "lẫn Hiragana", errorRef: M39 }
|
|
14
|
+
- { id: CHK-KANA-I3, value: "山田", reason: "lẫn Kanji", errorRef: M39 }
|
|
15
|
+
- { id: CHK-KANA-I4, value: "ヤマダ", reason: "half-width kana (nếu spec chỉ nhận full-width)", errorRef: M39 }
|
|
16
|
+
- { id: CHK-KANA-I5, value: "ゝ々", reason: "ký tự lặp — có tiền lệ lỗi thật nhiều hệ thống", errorRef: M39 }
|
|
17
|
+
- { id: CHK-KANA-I6, value: "ャュョ", reason: "kana nhỏ đứng một mình / sai vị trí", errorRef: M39 }
|
|
18
18
|
notes:
|
|
19
|
-
- "
|
|
19
|
+
- "Chú ý: kana nhỏ (ゃゅょ), trường âm (ー), dakuten/handakuten, half↔full-width kana."
|
|
20
20
|
uses_security: [sqli, xss]
|
|
@@ -1,20 +1,20 @@
|
|
|
1
1
|
# Field-type: number (float) — from common-checklist.xlsx (Input box · Number_Float).
|
|
2
2
|
type: number-float
|
|
3
|
-
description:
|
|
3
|
+
description: Số thập phân — chữ số + dấu thập phân; số chữ số phần lẻ theo spec.
|
|
4
4
|
constraints:
|
|
5
5
|
min: 0.0 # placeholder
|
|
6
6
|
max: 999999.99 # placeholder
|
|
7
7
|
decimalPlaces: 2 # placeholder
|
|
8
8
|
valid:
|
|
9
|
-
- { id: CHK-FLOAT-01, value: "123.45", note: "
|
|
10
|
-
- { id: CHK-FLOAT-02, value: "0.5", note: "
|
|
9
|
+
- { id: CHK-FLOAT-01, value: "123.45", note: "hợp lệ" }
|
|
10
|
+
- { id: CHK-FLOAT-02, value: "0.5", note: "phần nguyên = 0" }
|
|
11
11
|
boundary:
|
|
12
|
-
- { id: CHK-FLOAT-B1, gen: "
|
|
13
|
-
- { id: CHK-FLOAT-B2, gen: "
|
|
14
|
-
- { id: CHK-FLOAT-B3, gen: "
|
|
12
|
+
- { id: CHK-FLOAT-B1, gen: "Min / Min+1" }
|
|
13
|
+
- { id: CHK-FLOAT-B2, gen: "Max / Max-1" }
|
|
14
|
+
- { id: CHK-FLOAT-B3, gen: "đúng số chữ số thập phân / thừa 1 chữ số" }
|
|
15
15
|
invalid:
|
|
16
|
-
- { id: CHK-FLOAT-I1, gen: "
|
|
17
|
-
- { id: CHK-FLOAT-I2, gen: "
|
|
18
|
-
- { id: CHK-FLOAT-I3, gen: "
|
|
19
|
-
- { id: CHK-FLOAT-I4, value: "", reason: "
|
|
16
|
+
- { id: CHK-FLOAT-I1, gen: "chữ cái", reason: "không phải số", errorRef: M13 }
|
|
17
|
+
- { id: CHK-FLOAT-I2, gen: "format sai (nhiều dấu chấm, +/- sai vị trí)", reason: "format sai", errorRef: M13 }
|
|
18
|
+
- { id: CHK-FLOAT-I3, gen: "Min-1 / Max+1", reason: "vượt biên", errorRef: M13 }
|
|
19
|
+
- { id: CHK-FLOAT-I4, value: "", reason: "rỗng/multi-space (tuỳ spec)", errorRef: M02 }
|
|
20
20
|
uses_security: [sqli]
|
|
@@ -1,21 +1,21 @@
|
|
|
1
1
|
# Field-type: number (integer) — from common-checklist.xlsx (Input box · Number_Integer).
|
|
2
2
|
type: number-integer
|
|
3
|
-
description:
|
|
3
|
+
description: Số nguyên — chỉ chữ số, có thể có +/- ở vị trí hợp lệ.
|
|
4
4
|
constraints:
|
|
5
|
-
min: 0 # placeholder
|
|
5
|
+
min: 0 # placeholder theo spec
|
|
6
6
|
max: 2147483647 # placeholder
|
|
7
7
|
valid:
|
|
8
|
-
- { id: CHK-INT-01, value: "123", note: "
|
|
9
|
-
- { id: CHK-INT-02, value: "-45", note: "
|
|
8
|
+
- { id: CHK-INT-01, value: "123", note: "chỉ số" }
|
|
9
|
+
- { id: CHK-INT-02, value: "-45", note: "có dấu - đầu (nếu spec cho phép âm)" }
|
|
10
10
|
boundary:
|
|
11
|
-
- { id: CHK-INT-B1, gen: "
|
|
12
|
-
- { id: CHK-INT-B2, gen: "
|
|
11
|
+
- { id: CHK-INT-B1, gen: "Min / Min+1" }
|
|
12
|
+
- { id: CHK-INT-B2, gen: "Max / Max-1" }
|
|
13
13
|
invalid:
|
|
14
|
-
- { id: CHK-INT-I1, gen: "
|
|
15
|
-
- { id: CHK-INT-I2, gen: "full-width (Japanese)
|
|
16
|
-
- { id: CHK-INT-I3, gen: "+/-
|
|
17
|
-
- { id: CHK-INT-I4, gen: "
|
|
18
|
-
- { id: CHK-INT-I5, value: "", reason: "
|
|
14
|
+
- { id: CHK-INT-I1, gen: "chữ cái", reason: "không phải số", errorRef: M13 }
|
|
15
|
+
- { id: CHK-INT-I2, gen: "số full-width (Japanese number)", reason: "full-width", errorRef: M39 }
|
|
16
|
+
- { id: CHK-INT-I3, gen: "+/- sai vị trí (giữa chuỗi)", reason: "format sai", errorRef: M13 }
|
|
17
|
+
- { id: CHK-INT-I4, gen: "Min-1 / Max+1", reason: "vượt biên", errorRef: M13 }
|
|
18
|
+
- { id: CHK-INT-I5, value: "", reason: "rỗng/multi-space (tuỳ spec)", errorRef: M02 }
|
|
19
19
|
notes:
|
|
20
|
-
- "
|
|
20
|
+
- "leading zero (007) có bị cắt không; redundant spaces trước/sau phải xử lý đúng."
|
|
21
21
|
uses_security: [sqli]
|
|
@@ -1,30 +1,30 @@
|
|
|
1
|
-
# Field-type: password — from common-checklist.xlsx (Input box · Password).
|
|
1
|
+
# Field-type: password — seeded from common-checklist.xlsx (Input box · Password) + phuong-phap example.
|
|
2
2
|
type: password
|
|
3
|
-
description: Password —
|
|
4
|
-
format: "
|
|
3
|
+
description: Password — độ dài + thành phần ký tự theo spec; kèm show/hide + mask.
|
|
4
|
+
format: "theo ràng buộc spec (min/max, ≥1 hoa/thường/số tuỳ dự án)"
|
|
5
5
|
constraints:
|
|
6
|
-
minLength: 7 # placeholder —
|
|
6
|
+
minLength: 7 # placeholder — ghi đè bằng min thật của spec dự án
|
|
7
7
|
maxLength: 20 # placeholder
|
|
8
|
-
requires: [uppercase, lowercase, digit] # placeholder
|
|
8
|
+
requires: [uppercase, lowercase, digit] # placeholder theo spec
|
|
9
9
|
uses_security: [sqli, xss]
|
|
10
10
|
valid:
|
|
11
|
-
- { id: CHK-PW-01, value: "Passw0rd", note: "
|
|
12
|
-
- { id: CHK-PW-02, note: "alpha only / numeric only / mix —
|
|
13
|
-
- { id: CHK-PW-03, note: "
|
|
11
|
+
- { id: CHK-PW-01, value: "Passw0rd", note: "đủ hoa+thường+số, trong khoảng" }
|
|
12
|
+
- { id: CHK-PW-02, note: "alpha only / numeric only / mix — nếu spec cho phép" }
|
|
13
|
+
- { id: CHK-PW-03, note: "ký tự đặc biệt hợp lệ theo spec (vd ._@)" }
|
|
14
14
|
boundary:
|
|
15
|
-
- { id: CHK-PW-B1, gen: "
|
|
16
|
-
- { id: CHK-PW-B2, gen: "
|
|
17
|
-
- { id: CHK-PW-B3, gen: "
|
|
18
|
-
- { id: CHK-PW-B4, gen: "
|
|
15
|
+
- { id: CHK-PW-B1, gen: "đúng minLength", note: "min (vd Aa1bcde = 7)" }
|
|
16
|
+
- { id: CHK-PW-B2, gen: "minLength+1", note: "min+1" }
|
|
17
|
+
- { id: CHK-PW-B3, gen: "đúng maxLength", note: "max" }
|
|
18
|
+
- { id: CHK-PW-B4, gen: "maxLength-1", note: "max-1" }
|
|
19
19
|
invalid:
|
|
20
|
-
- { id: CHK-PW-I1, value: "", reason: "
|
|
21
|
-
- { id: CHK-PW-I2, gen: "<
|
|
22
|
-
- { id: CHK-PW-I3, gen: ">
|
|
23
|
-
- { id: CHK-PW-I4, value: "passw0rd", reason: "
|
|
24
|
-
- { id: CHK-PW-I5, value: "PASSW0RD", reason: "
|
|
25
|
-
- { id: CHK-PW-I6, value: "Password", reason: "
|
|
26
|
-
- { id: CHK-PW-I7, gen: "full-width Japanese", reason: "full-width
|
|
27
|
-
- { id: CHK-PW-I8, gen: "space before/after
|
|
20
|
+
- { id: CHK-PW-I1, value: "", reason: "rỗng/multi-space (required)", errorRef: M02 }
|
|
21
|
+
- { id: CHK-PW-I2, gen: "< minLength", reason: "dưới độ dài tối thiểu", errorRef: M29 }
|
|
22
|
+
- { id: CHK-PW-I3, gen: "> maxLength", reason: "vượt độ dài tối đa", errorRef: M30 }
|
|
23
|
+
- { id: CHK-PW-I4, value: "passw0rd", reason: "thiếu chữ hoa", errorRef: M31 }
|
|
24
|
+
- { id: CHK-PW-I5, value: "PASSW0RD", reason: "thiếu chữ thường", errorRef: M32 }
|
|
25
|
+
- { id: CHK-PW-I6, value: "Password", reason: "thiếu chữ số", errorRef: M33 }
|
|
26
|
+
- { id: CHK-PW-I7, gen: "full-width Japanese", reason: "ký tự full-width", errorRef: M39 }
|
|
27
|
+
- { id: CHK-PW-I8, gen: "space before/after hoặc giữa", reason: "khoảng trắng thừa", errorRef: M13 }
|
|
28
28
|
notes:
|
|
29
|
-
- "
|
|
30
|
-
- "Confirm Password:
|
|
29
|
+
- "Kiểm tra riêng: icon show/hide password (hiển thị + toggle mask↔unmask), ký tự mask đúng spec."
|
|
30
|
+
- "Confirm Password: kế thừa toàn bộ case của password + case (confirm == password) và (confirm != password → lỗi)."
|
|
@@ -1,23 +1,23 @@
|
|
|
1
1
|
# Field-type: phone number — from common-checklist.xlsx (Input box · Phone number).
|
|
2
2
|
type: phone
|
|
3
|
-
description:
|
|
3
|
+
description: Số điện thoại — chỉ số, cho phép () - + ở vị trí hợp lệ.
|
|
4
4
|
constraints:
|
|
5
5
|
charset: "digits + () - +"
|
|
6
|
-
minLength: 8 # placeholder
|
|
6
|
+
minLength: 8 # placeholder theo spec
|
|
7
7
|
maxLength: 15 # placeholder
|
|
8
8
|
valid:
|
|
9
|
-
- { id: CHK-PHONE-01, value: "0987654321", note: "
|
|
10
|
-
- { id: CHK-PHONE-02, value: "+84-90-1234567", note: "+
|
|
11
|
-
- { id: CHK-PHONE-03, value: "(028)38221234", note: "()
|
|
9
|
+
- { id: CHK-PHONE-01, value: "0987654321", note: "chỉ số" }
|
|
10
|
+
- { id: CHK-PHONE-02, value: "+84-90-1234567", note: "có + và - đúng vị trí" }
|
|
11
|
+
- { id: CHK-PHONE-03, value: "(028)38221234", note: "có () đúng vị trí" }
|
|
12
12
|
boundary:
|
|
13
|
-
- { id: CHK-PHONE-B1, gen: "min / min+1
|
|
14
|
-
- { id: CHK-PHONE-B2, gen: "max / max-1
|
|
13
|
+
- { id: CHK-PHONE-B1, gen: "đúng min / min+1" }
|
|
14
|
+
- { id: CHK-PHONE-B2, gen: "đúng max / max-1" }
|
|
15
15
|
invalid:
|
|
16
|
-
- { id: CHK-PHONE-I1, gen: "
|
|
17
|
-
- { id: CHK-PHONE-I2, gen: "full-width
|
|
18
|
-
- { id: CHK-PHONE-I3, gen: "
|
|
19
|
-
- { id: CHK-PHONE-I4, value: "090 123 4567", reason: "
|
|
20
|
-
- { id: CHK-PHONE-I5, gen: "+/-
|
|
21
|
-
- { id: CHK-PHONE-I6, gen: "min-1 / max+1", reason: "
|
|
22
|
-
- { id: CHK-PHONE-I7, value: "", reason: "
|
|
16
|
+
- { id: CHK-PHONE-I1, gen: "ký tự cấm []:,;\"<>\\?", reason: "ký tự không hợp lệ", errorRef: M13 }
|
|
17
|
+
- { id: CHK-PHONE-I2, gen: "số Nhật (full-width)", reason: "ký tự full-width", errorRef: M39 }
|
|
18
|
+
- { id: CHK-PHONE-I3, gen: "chữ cái", reason: "không phải số", errorRef: M13 }
|
|
19
|
+
- { id: CHK-PHONE-I4, value: "090 123 4567", reason: "khoảng trắng giữa số", errorRef: M13 }
|
|
20
|
+
- { id: CHK-PHONE-I5, gen: "+/- không ở vị trí đầu; () sai vị trí", reason: "format sai", errorRef: M13 }
|
|
21
|
+
- { id: CHK-PHONE-I6, gen: "min-1 / max+1", reason: "vượt biên", errorRef: M13 }
|
|
22
|
+
- { id: CHK-PHONE-I7, value: "", reason: "rỗng/multi-space (tuỳ spec)", errorRef: M02 }
|
|
23
23
|
uses_security: [sqli]
|
|
@@ -1,16 +1,16 @@
|
|
|
1
1
|
# Field-type: postcode / zipcode — from common-checklist.xlsx (Input box · Postcode/Zipcode).
|
|
2
2
|
type: postcode
|
|
3
|
-
description:
|
|
3
|
+
description: Mã bưu chính — format theo vùng (JP 7 số, có thể có dấu -).
|
|
4
4
|
constraints:
|
|
5
|
-
format: "region-specific (
|
|
5
|
+
format: "region-specific (vd JP: NNN-NNNN)"
|
|
6
6
|
length: 7 # placeholder
|
|
7
7
|
valid:
|
|
8
|
-
- { id: CHK-ZIP-01, value: "1500001", note: "7
|
|
9
|
-
- { id: CHK-ZIP-02, value: "150-0001", note: "
|
|
8
|
+
- { id: CHK-ZIP-01, value: "1500001", note: "7 số (JP)" }
|
|
9
|
+
- { id: CHK-ZIP-02, value: "150-0001", note: "có dấu - nếu spec cho phép" }
|
|
10
10
|
boundary:
|
|
11
|
-
- { id: CHK-ZIP-B1, gen: "
|
|
11
|
+
- { id: CHK-ZIP-B1, gen: "đúng độ dài / độ dài-1 / +1" }
|
|
12
12
|
invalid:
|
|
13
|
-
- { id: CHK-ZIP-I1, gen: "
|
|
14
|
-
- { id: CHK-ZIP-I2, gen: "
|
|
15
|
-
- { id: CHK-ZIP-I3, value: "", reason: "
|
|
13
|
+
- { id: CHK-ZIP-I1, gen: "chữ cái / ký tự lạ", reason: "sai ký tự", errorRef: M13 }
|
|
14
|
+
- { id: CHK-ZIP-I2, gen: "sai format vùng", reason: "sai format", errorRef: M13 }
|
|
15
|
+
- { id: CHK-ZIP-I3, value: "", reason: "rỗng (tuỳ spec)", errorRef: M02 }
|
|
16
16
|
uses_security: [sqli]
|
|
@@ -1,20 +1,20 @@
|
|
|
1
|
-
# Field-type: string (
|
|
1
|
+
# Field-type: string (text 1 dòng) — from common-checklist.xlsx (Input box · String).
|
|
2
2
|
type: string
|
|
3
|
-
description:
|
|
3
|
+
description: Chuỗi 1 dòng — chấp nhận alnum, JP half/full-width, special, unicode, space.
|
|
4
4
|
constraints:
|
|
5
|
-
minLength: 1 # placeholder
|
|
5
|
+
minLength: 1 # placeholder theo spec
|
|
6
6
|
maxLength: 255 # placeholder
|
|
7
7
|
uses_security: [sqli, xss]
|
|
8
8
|
valid:
|
|
9
9
|
- { id: CHK-STR-01, value: "Normal text 123", note: "alnum + space" }
|
|
10
|
-
- { id: CHK-STR-02, gen: "half-width Japanese", note: "
|
|
10
|
+
- { id: CHK-STR-02, gen: "half-width Japanese", note: "半角カナ" }
|
|
11
11
|
- { id: CHK-STR-03, gen: "full-width Japanese", note: "全角かな漢字" }
|
|
12
|
-
- { id: CHK-STR-04, gen: "special chars ~!@#$%^&*()_+{}:|<>?,./;'[]-=`", note: "
|
|
13
|
-
- { id: CHK-STR-05, gen: "unicode / emoji
|
|
14
|
-
- { id: CHK-STR-06, gen: "copy-paste full-width JP", note: "
|
|
12
|
+
- { id: CHK-STR-04, gen: "special chars ~!@#$%^&*()_+{}:|<>?,./;'[]-=`", note: "ký tự đặc biệt hợp lệ" }
|
|
13
|
+
- { id: CHK-STR-05, gen: "unicode / emoji nếu spec cho phép" }
|
|
14
|
+
- { id: CHK-STR-06, gen: "copy-paste full-width JP", note: "phải xử lý đúng khi paste" }
|
|
15
15
|
boundary:
|
|
16
|
-
- { id: CHK-STR-B1, gen: "min / min+1
|
|
17
|
-
- { id: CHK-STR-B2, gen: "max / max-1
|
|
16
|
+
- { id: CHK-STR-B1, gen: "đúng min / min+1" }
|
|
17
|
+
- { id: CHK-STR-B2, gen: "đúng max / max-1" }
|
|
18
18
|
invalid:
|
|
19
|
-
- { id: CHK-STR-I1, value: "", reason: "
|
|
20
|
-
- { id: CHK-STR-I2, gen: "min-1 / max+1", reason: "
|
|
19
|
+
- { id: CHK-STR-I1, value: "", reason: "rỗng/multi-space (success/fail tuỳ spec)", errorRef: M02 }
|
|
20
|
+
- { id: CHK-STR-I2, gen: "min-1 / max+1", reason: "vượt biên", errorRef: M13 }
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
# Security payload bank: Broken authentication — from common-checklist.xlsx (Broken authentication).
|
|
2
2
|
class: broken-auth
|
|
3
|
-
description:
|
|
3
|
+
description: Xác thực yếu — session/token/brute-force.
|
|
4
4
|
payloads:
|
|
5
|
-
- { id: CHK-SEC-AUTH-01, technique: "
|
|
6
|
-
- { id: CHK-SEC-AUTH-02, technique: "session
|
|
7
|
-
- { id: CHK-SEC-AUTH-03, technique: "session fixation —
|
|
8
|
-
- { id: CHK-SEC-AUTH-04, technique: "
|
|
9
|
-
oracle: "
|
|
5
|
+
- { id: CHK-SEC-AUTH-01, technique: "brute force login không giới hạn", expect: "có lockout/captcha/rate-limit" }
|
|
6
|
+
- { id: CHK-SEC-AUTH-02, technique: "session không hết hạn sau logout / đổi mật khẩu", expect: "invalidate session cũ" }
|
|
7
|
+
- { id: CHK-SEC-AUTH-03, technique: "session fixation — dùng lại session id trước đăng nhập", expect: "cấp session mới sau login" }
|
|
8
|
+
- { id: CHK-SEC-AUTH-04, technique: "token đoán được / không hết hạn", expect: "token ngẫu nhiên mạnh, có TTL" }
|
|
9
|
+
oracle: "Đăng nhập/duy trì phiên trái phép = lỗ hổng."
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
# Security payload bank: IDOR — from common-checklist.xlsx (Insecure Direct Object Reference).
|
|
2
2
|
class: idor
|
|
3
|
-
description: Insecure Direct Object Reference —
|
|
3
|
+
description: Insecure Direct Object Reference — đổi id để truy cập tài nguyên của người khác.
|
|
4
4
|
payloads:
|
|
5
|
-
- { id: CHK-SEC-IDOR-01, technique: "
|
|
6
|
-
- { id: CHK-SEC-IDOR-02, technique: "
|
|
7
|
-
- { id: CHK-SEC-IDOR-03, technique: "
|
|
8
|
-
oracle: "
|
|
5
|
+
- { id: CHK-SEC-IDOR-01, technique: "đổi id trên URL/param sang record của user khác", expect: "403/404, KHÔNG lộ dữ liệu người khác" }
|
|
6
|
+
- { id: CHK-SEC-IDOR-02, technique: "đoán id tuần tự (1,2,3…) trên API detail", expect: "kiểm tra quyền sở hữu, từ chối" }
|
|
7
|
+
- { id: CHK-SEC-IDOR-03, technique: "thao tác (edit/delete) resource ngoài quyền qua id", expect: "chặn ở server, không chỉ ẩn UI" }
|
|
8
|
+
oracle: "Truy cập/sửa được dữ liệu ngoài quyền = lỗ hổng. Kỳ vọng: authorization check theo ownership phía server."
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
# Security payload bank: Input validation — from common-checklist.xlsx (Input validation).
|
|
2
2
|
class: input-validation
|
|
3
|
-
description:
|
|
3
|
+
description: Ràng buộc đầu vào phía server (không chỉ FE).
|
|
4
4
|
payloads:
|
|
5
|
-
- { id: CHK-SEC-IV-01, technique: "
|
|
6
|
-
- { id: CHK-SEC-IV-02, technique: "
|
|
7
|
-
- { id: CHK-SEC-IV-03, technique: "control/null bytes,
|
|
8
|
-
oracle: "
|
|
5
|
+
- { id: CHK-SEC-IV-01, technique: "vượt boundary khi FE bị bypass (gọi API trực tiếp)", expect: "server cũng chặn" }
|
|
6
|
+
- { id: CHK-SEC-IV-02, technique: "sai kiểu dữ liệu (string vào field số)", expect: "reject với lỗi hợp lệ" }
|
|
7
|
+
- { id: CHK-SEC-IV-03, technique: "control/null bytes, ký tự điều khiển", expect: "loại bỏ/reject" }
|
|
8
|
+
oracle: "FE qua được nhưng BE phải chặn — test trực tiếp API bỏ qua FE."
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
# Security payload bank: SQL Injection — from common-checklist.xlsx (SQL Injection sheet).
|
|
2
2
|
# Reusable adversarial values injected into the `invalid` group of any field via uses_security: [sqli].
|
|
3
3
|
class: sqli
|
|
4
|
-
description: SQL Injection —
|
|
4
|
+
description: SQL Injection — dữ liệu đầu vào nhằm phá câu truy vấn / bypass xác thực.
|
|
5
5
|
payloads:
|
|
6
|
-
- { id: CHK-SEC-SQLI-01, value: "admin' --", target: "login username", expect: "
|
|
7
|
-
- { id: CHK-SEC-SQLI-02, value: "test' OR '1'='1", target: "
|
|
8
|
-
- { id: CHK-SEC-SQLI-03, value: "test@test.com' OR '1'='1", target: "
|
|
9
|
-
- { id: CHK-SEC-SQLI-04, value: "test', isAdmin = 1 --", target: "
|
|
10
|
-
- { id: CHK-SEC-SQLI-05, value: "2023-01-01' OR '1'='1", target: "report
|
|
11
|
-
oracle: "
|
|
6
|
+
- { id: CHK-SEC-SQLI-01, value: "admin' --", target: "login username", expect: "KHÔNG được đăng nhập bỏ qua mật khẩu" }
|
|
7
|
+
- { id: CHK-SEC-SQLI-02, value: "test' OR '1'='1", target: "ô tìm kiếm", expect: "KHÔNG trả về toàn bộ dữ liệu" }
|
|
8
|
+
- { id: CHK-SEC-SQLI-03, value: "test@test.com' OR '1'='1", target: "email đăng ký", expect: "xử lý hợp lệ, không lưu sai dữ liệu" }
|
|
9
|
+
- { id: CHK-SEC-SQLI-04, value: "test', isAdmin = 1 --", target: "sửa hồ sơ", expect: "KHÔNG nâng quyền trái phép" }
|
|
10
|
+
- { id: CHK-SEC-SQLI-05, value: "2023-01-01' OR '1'='1", target: "bộ lọc report", expect: "chỉ trả dữ liệu đúng filter" }
|
|
11
|
+
oracle: "Bất kỳ dấu hiệu bypass/trả sai dữ liệu = lỗ hổng. Kỳ vọng: escape/parametrize an toàn, báo lỗi hợp lệ."
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
# Security payload bank: Upload file — from common-checklist.xlsx (Upload file function).
|
|
2
2
|
class: upload
|
|
3
|
-
description:
|
|
3
|
+
description: Upload file độc / vượt ràng buộc.
|
|
4
4
|
payloads:
|
|
5
|
-
- { id: CHK-SEC-UP-01, technique: "extension
|
|
6
|
-
- { id: CHK-SEC-UP-02, technique: "MIME mismatch (
|
|
7
|
-
- { id: CHK-SEC-UP-03, technique: "oversize
|
|
8
|
-
- { id: CHK-SEC-UP-04, technique: "path traversal
|
|
9
|
-
- { id: CHK-SEC-UP-05, technique: "
|
|
10
|
-
oracle: "
|
|
5
|
+
- { id: CHK-SEC-UP-01, technique: "file trá hình extension (shell.php.jpg)", expect: "kiểm tra MIME thật + whitelist, từ chối" }
|
|
6
|
+
- { id: CHK-SEC-UP-02, technique: "MIME mismatch (đổi Content-Type)", expect: "verify nội dung thật" }
|
|
7
|
+
- { id: CHK-SEC-UP-03, technique: "oversize vượt giới hạn", expect: "báo lỗi kích thước" }
|
|
8
|
+
- { id: CHK-SEC-UP-04, technique: "path traversal trong tên file (../../)", expect: "sanitize tên file, lưu đúng thư mục" }
|
|
9
|
+
- { id: CHK-SEC-UP-05, technique: "tên file chứa <script> / metadata độc", expect: "xử lý an toàn, không render ra DOM" }
|
|
10
|
+
oracle: "Thực thi/ghi ngoài phạm vi = lỗ hổng."
|
package/common/security/xss.yaml
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
# Security payload bank: XSS Injection — from common-checklist.xlsx (XSS Injection sheet).
|
|
2
2
|
class: xss
|
|
3
|
-
description: Cross-Site Scripting —
|
|
3
|
+
description: Cross-Site Scripting — chèn script qua input/URL/attribute/file để thực thi trái phép.
|
|
4
4
|
payloads:
|
|
5
|
-
- { id: CHK-SEC-XSS-01, value: "<script>alert('XSS')</script>", kind: "Reflected/Stored", expect: "
|
|
6
|
-
- { id: CHK-SEC-XSS-02, value: "<img src=x onerror=alert(1)>", kind: "HTML attribute", expect: "
|
|
7
|
-
- { id: CHK-SEC-XSS-03, value: "<svg onload=alert(1)>", kind: "SVG payload", expect: "
|
|
8
|
-
- { id: CHK-SEC-XSS-04, value: "\"><script>alert(1)</script>", kind: "Attribute injection", expect: "
|
|
9
|
-
- { id: CHK-SEC-XSS-05, value: "<a href=\"javascript:alert(1)\">click me</a>", kind: "Dangerous link", expect: "href
|
|
10
|
-
- { id: CHK-SEC-XSS-06, value: "<script>alert(1)</script>.jpg", kind: "File name / metadata", expect: "
|
|
11
|
-
- { id: CHK-SEC-XSS-07, value: "<h1>test</h1>", kind: "HTML injection", expect: "
|
|
12
|
-
oracle: "
|
|
5
|
+
- { id: CHK-SEC-XSS-01, value: "<script>alert('XSS')</script>", kind: "Reflected/Stored", expect: "hiển thị text thuần, KHÔNG thực thi script" }
|
|
6
|
+
- { id: CHK-SEC-XSS-02, value: "<img src=x onerror=alert(1)>", kind: "HTML attribute", expect: "không thực thi onerror" }
|
|
7
|
+
- { id: CHK-SEC-XSS-03, value: "<svg onload=alert(1)>", kind: "SVG payload", expect: "không thực thi onload / loại bỏ thuộc tính" }
|
|
8
|
+
- { id: CHK-SEC-XSS-04, value: "\"><script>alert(1)</script>", kind: "Attribute injection", expect: "không phá vỡ HTML, escape an toàn" }
|
|
9
|
+
- { id: CHK-SEC-XSS-05, value: "<a href=\"javascript:alert(1)\">click me</a>", kind: "Dangerous link", expect: "href bị vô hiệu / sanitize" }
|
|
10
|
+
- { id: CHK-SEC-XSS-06, value: "<script>alert(1)</script>.jpg", kind: "File name / metadata", expect: "tên file xử lý an toàn, không render ra DOM" }
|
|
11
|
+
- { id: CHK-SEC-XSS-07, value: "<h1>test</h1>", kind: "HTML injection", expect: "hiển thị dạng text, không render thẻ" }
|
|
12
|
+
oracle: "Alert hiển thị / thẻ được render = lỗ hổng. Kỳ vọng: escape/sanitize, không innerHTML/eval user input."
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
# Technique: Boundary Value Analysis — from
|
|
1
|
+
# Technique: Boundary Value Analysis — from phuong-phap §Nguồn-3.
|
|
2
2
|
technique: boundary
|
|
3
|
-
description:
|
|
4
|
-
rule: "
|
|
3
|
+
description: Khi field có min/max rõ ràng — sinh giá trị tại/quanh biên.
|
|
4
|
+
rule: "Với [min,max]: sinh Min-1 (invalid), Min, Min+1, Max-1, Max, Max+1 (invalid)."
|
|
5
5
|
applies_to: [number-integer, number-float, currency, string, password, phone, postcode]
|
|
6
|
-
note: "
|
|
6
|
+
note: "Phải có 'đúng biên' (Min, Max), không chỉ 'quá biên' — off-by-one thường lọt ở đây."
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
# Technique: Decision Table — from
|
|
1
|
+
# Technique: Decision Table — from phuong-phap §Nguồn-3.
|
|
2
2
|
technique: decision-table
|
|
3
|
-
description:
|
|
4
|
-
rule: "
|
|
3
|
+
description: Nhiều điều kiện kết hợp cùng ảnh hưởng 1 kết quả.
|
|
4
|
+
rule: "Liệt kê tổ hợp điều kiện → hành động; sinh @cases theo mỗi rule row."
|
|
5
5
|
applies_to: [login, permission, business-flow]
|
|
6
|
-
example: "Login:
|
|
6
|
+
example: "Login: mật khẩu đúng/sai × account khoá/không × 2FA bật/tắt."
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
# Technique: Equivalence Partitioning — from
|
|
1
|
+
# Technique: Equivalence Partitioning — from phuong-phap §Nguồn-3.
|
|
2
2
|
technique: equivalence
|
|
3
|
-
description:
|
|
4
|
-
rule: "
|
|
3
|
+
description: Input chia thành nhóm cùng hành vi — chỉ cần 1 đại diện mỗi nhóm.
|
|
4
|
+
rule: "Gom giá trị theo vùng tương đương (valid / mỗi loại invalid) → 1 đại diện/vùng, tránh test tràn lan."
|
|
5
5
|
applies_to: [all]
|
|
6
|
-
note: "
|
|
6
|
+
note: "Không cần 300 email sai — chỉ 1 đại diện cho mỗi CÁCH sai (thiếu @, sai vị trí ., full-width…)."
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
# Technique: State Transition + Dependency State Coverage — from
|
|
1
|
+
# Technique: State Transition + Dependency State Coverage — from phuong-phap §Nguồn-3.
|
|
2
2
|
technique: state-transition
|
|
3
|
-
description:
|
|
4
|
-
rule: "
|
|
3
|
+
description: Đối tượng nhiều trạng thái; hoặc A phụ thuộc trạng thái của B.
|
|
4
|
+
rule: "Phủ chuyển hợp lệ + chuyển NGƯỢC không hợp lệ; test X với B ở đủ trạng thái (active/chưa active/hết hạn/xoá mềm)."
|
|
5
5
|
applies_to: [workflow-entity, dependent-feature]
|
|
6
|
-
example: "Draft
|
|
6
|
+
example: "Draft → Submitted → Approved → Shipped (kể cả chuyển ngược không hợp lệ)."
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sungen/driver-data-factory",
|
|
3
|
-
"version": "3.2.10-beta.
|
|
3
|
+
"version": "3.2.10-beta.2",
|
|
4
4
|
"description": "Sungen Data Factory capability — standardized test-data generation from a field-type + security knowledge catalog (valid/boundary/invalid + traceability), the .overwrite merge layer, and (opt-in) @dataFactory runtime seeding. Design-time knowledge is runtime-free. Plugs into @sun-asterisk/sungen via the capability SPI.",
|
|
5
5
|
"main": "src/index.ts",
|
|
6
6
|
"types": "src/index.ts",
|
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
"author": "eqe team (engineer & quality) — Sun Asterisk",
|
|
15
15
|
"license": "MIT",
|
|
16
16
|
"dependencies": {
|
|
17
|
-
"@sun-asterisk/sungen": "3.2.10-beta.
|
|
17
|
+
"@sun-asterisk/sungen": "3.2.10-beta.2",
|
|
18
18
|
"yaml": "^2.4.0"
|
|
19
19
|
},
|
|
20
20
|
"files": [
|
package/src/catalog-loader.ts
CHANGED
|
@@ -101,8 +101,8 @@ export function loadEffectiveCatalog(projectDir: string, root = commonDir()): {
|
|
|
101
101
|
if (!merged.security[bank]) {
|
|
102
102
|
warnings.push({
|
|
103
103
|
fieldType: type,
|
|
104
|
-
detail: `overwrite
|
|
105
|
-
suggestion: `
|
|
104
|
+
detail: `overwrite tham chiếu security bank "${bank}" không còn trong common`,
|
|
105
|
+
suggestion: `cập nhật hoặc xoá mục "${type}" trong qa/data-factory/common.overwrite.yaml`,
|
|
106
106
|
});
|
|
107
107
|
}
|
|
108
108
|
}
|
package/src/cli.ts
CHANGED
|
@@ -13,8 +13,6 @@ import { parse as parseYaml, stringify as stringifyYaml } from 'yaml';
|
|
|
13
13
|
import { loadEffectiveCatalog } from './catalog-loader';
|
|
14
14
|
import { expandFieldMap, renderTestData, type FieldMap } from './generator';
|
|
15
15
|
import { lintFieldMap, summarize } from './lint';
|
|
16
|
-
import { generateFixture } from './fixture';
|
|
17
|
-
import { ensureDataFactoryReadme } from './readme';
|
|
18
16
|
|
|
19
17
|
const FIELDMAP_DIR = (cwd: string) => join(cwd, 'qa', 'data-factory');
|
|
20
18
|
const fieldMapPath = (cwd: string, name: string) => join(FIELDMAP_DIR(cwd), `${name}.fields.yaml`);
|
|
@@ -57,31 +55,14 @@ async function genOne(cwd: string, name: string, opts: { force?: boolean }): Pro
|
|
|
57
55
|
const out = testDataPath(cwd, name);
|
|
58
56
|
if (!out) { console.error(` ✗ ${name}: no qa/screens/${name} or qa/flows/${name}`); return 'missing'; }
|
|
59
57
|
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
// MERGE, don't clobber. Data Factory owns only the keys it generates (`_generated_by` + each
|
|
64
|
-
// field name); every OTHER key in the existing test-data (create-test's data, flow-namespaced
|
|
65
|
-
// keys like `login.email`, @cases datasets, overrides) is preserved. A regenerated field key is
|
|
66
|
-
// replaced by its standardized version (that's the point). Fixes the whole-file clobber.
|
|
67
|
-
let final = dfData;
|
|
68
|
-
let mergedInto = false;
|
|
69
|
-
if (exists) {
|
|
70
|
-
if (!opts.force) {
|
|
71
|
-
const a = (await ask(` ? ${name}: test-data exists — merge standardized field data in (keeps other keys)? [y/N] `)).toLowerCase();
|
|
72
|
-
if (a !== 'y' && a !== 'yes') { console.log(` · ${name}: skipped`); return 'skipped'; }
|
|
73
|
-
}
|
|
74
|
-
let existing: Record<string, any> = {};
|
|
75
|
-
try { existing = (parseYaml(readFileSync(out, 'utf8')) as Record<string, any>) || {}; } catch { existing = {}; }
|
|
76
|
-
final = { ...existing, ...dfData }; // DF-owned keys win; all other existing keys preserved
|
|
77
|
-
mergedInto = true;
|
|
58
|
+
if (existsSync(out) && !opts.force) {
|
|
59
|
+
const a = (await ask(` ? ${name}: test-data already exists — overwrite? [y/N] `)).toLowerCase();
|
|
60
|
+
if (a !== 'y' && a !== 'yes') { console.log(` · ${name}: skipped`); return 'skipped'; }
|
|
78
61
|
}
|
|
79
62
|
mkdirSync(dirname(out), { recursive: true });
|
|
80
|
-
writeFileSync(out, stringifyYaml(
|
|
63
|
+
writeFileSync(out, stringifyYaml(renderTestData(expanded)), 'utf8');
|
|
81
64
|
const blind = expanded.filter((f) => f.blindSpot).map((f) => f.name);
|
|
82
|
-
|
|
83
|
-
const fieldKeys = expanded.map((f) => f.name).length;
|
|
84
|
-
console.log(` ✓ ${name}: ${verb} ${out.replace(cwd + '/', '')} (${fieldKeys} field key(s)${mergedInto ? ', other keys preserved' : ''})${blind.length ? ` — blind spots: ${blind.join(', ')}` : ''}`);
|
|
65
|
+
console.log(` ✓ ${name}: wrote ${out.replace(cwd + '/', '')}${blind.length ? ` (blind spots: ${blind.join(', ')})` : ''}`);
|
|
85
66
|
return 'written';
|
|
86
67
|
}
|
|
87
68
|
|
|
@@ -97,7 +78,6 @@ export function registerDataFactoryCommand(program: any): void {
|
|
|
97
78
|
.option('--force', 'Overwrite existing test-data without asking (implies --yes)')
|
|
98
79
|
.action(async (opts: { screen?: string; all?: boolean; yes?: boolean; force?: boolean }) => {
|
|
99
80
|
const cwd = process.cwd();
|
|
100
|
-
ensureDataFactoryReadme(cwd);
|
|
101
81
|
if (opts.screen) { await genOne(cwd, opts.screen, opts); return; }
|
|
102
82
|
if (!opts.all) {
|
|
103
83
|
console.error('Specify --screen <name> for one unit, or --all for every unit (with confirmation).');
|
|
@@ -125,7 +105,6 @@ export function registerDataFactoryCommand(program: any): void {
|
|
|
125
105
|
.option('--screen <name>', 'Lint one unit (default: all field-maps)')
|
|
126
106
|
.action((opts: { screen?: string }) => {
|
|
127
107
|
const cwd = process.cwd();
|
|
128
|
-
ensureDataFactoryReadme(cwd);
|
|
129
108
|
const names = opts.screen ? [opts.screen] : listFieldMaps(cwd);
|
|
130
109
|
if (names.length === 0) { console.error('No field-maps under qa/data-factory/*.fields.yaml.'); process.exitCode = 1; return; }
|
|
131
110
|
const { catalog } = loadEffectiveCatalog(cwd);
|
|
@@ -145,32 +124,4 @@ export function registerDataFactoryCommand(program: any): void {
|
|
|
145
124
|
}
|
|
146
125
|
if (totalErrors) process.exitCode = 1;
|
|
147
126
|
});
|
|
148
|
-
|
|
149
|
-
data
|
|
150
|
-
.command('fixture')
|
|
151
|
-
.description('Generate a CSV import fixture from the field-map (bulk-import / file-upload testing)')
|
|
152
|
-
.requiredOption('--screen <name>', 'Unit whose field-map to build a fixture from')
|
|
153
|
-
.option('--rows <n>', 'Number of valid rows', '3')
|
|
154
|
-
.option('--invalid', 'Also write <name>.invalid.csv (one seeded-bad row per field + _violation column)')
|
|
155
|
-
.option('--force', 'Overwrite existing fixture file(s) without asking')
|
|
156
|
-
.action(async (opts: { screen: string; rows?: string; invalid?: boolean; force?: boolean }) => {
|
|
157
|
-
const cwd = process.cwd();
|
|
158
|
-
ensureDataFactoryReadme(cwd);
|
|
159
|
-
const map = loadFieldMap(cwd, opts.screen);
|
|
160
|
-
if (!map) { console.error(`✗ ${opts.screen}: no field-map at qa/data-factory/${opts.screen}.fields.yaml`); process.exitCode = 1; return; }
|
|
161
|
-
const { catalog } = loadEffectiveCatalog(cwd);
|
|
162
|
-
const res = generateFixture(map, catalog, { rows: parseInt(opts.rows || '3', 10), includeInvalid: !!opts.invalid });
|
|
163
|
-
const dir = join(cwd, 'qa', 'fixtures');
|
|
164
|
-
mkdirSync(dir, { recursive: true });
|
|
165
|
-
const validPath = join(dir, `${opts.screen}.csv`);
|
|
166
|
-
const writes: Array<[string, string]> = [[validPath, res.valid]];
|
|
167
|
-
if (res.invalid) writes.push([join(dir, `${opts.screen}.invalid.csv`), res.invalid]);
|
|
168
|
-
for (const [p] of writes) {
|
|
169
|
-
if (existsSync(p) && !opts.force) {
|
|
170
|
-
const a = (await ask(` ? ${p.replace(cwd + '/', '')} exists — overwrite? [y/N] `)).toLowerCase();
|
|
171
|
-
if (a !== 'y' && a !== 'yes') { console.log(' · skipped'); return; }
|
|
172
|
-
}
|
|
173
|
-
}
|
|
174
|
-
for (const [p, content] of writes) { writeFileSync(p, content, 'utf8'); console.log(` ✓ wrote ${p.replace(cwd + '/', '')} (${res.headers.length} columns)`); }
|
|
175
|
-
});
|
|
176
127
|
}
|
package/src/generator.ts
CHANGED
|
@@ -13,31 +13,13 @@ import type { Catalog, FieldType, DataPoint } from './types';
|
|
|
13
13
|
|
|
14
14
|
export interface FieldDef {
|
|
15
15
|
name: string;
|
|
16
|
-
/** Catalog field-type id (email/password/…).
|
|
17
|
-
type
|
|
16
|
+
/** Catalog field-type id (email/password/…). A type absent from the catalog → blind spot. */
|
|
17
|
+
type: string;
|
|
18
18
|
required?: boolean;
|
|
19
19
|
/** Project overrides merged over the catalog constraints (maxLength, min, max, …). */
|
|
20
20
|
constraints?: Record<string, any>;
|
|
21
21
|
/** Placeholder errorRef (Mxx) → real project message code. Unmapped refs are flagged open. */
|
|
22
22
|
errorMap?: Record<string, string>;
|
|
23
|
-
/**
|
|
24
|
-
* Field must be fresh/unique per test run (e.g. a register email/username that can't collide
|
|
25
|
-
* with a record left by a previous run). When true, the VALID value(s) get a runtime `{{$timestamp}}`
|
|
26
|
-
* suffix so `resolveDynamicVars` produces a new value every run — no cross-run cache, no flakiness.
|
|
27
|
-
*/
|
|
28
|
-
unique?: boolean;
|
|
29
|
-
/**
|
|
30
|
-
* Nested / group field (complex form): sub-fields expanded recursively under this name
|
|
31
|
-
* (e.g. `address` → `city`, `postcode`). A field with `fields` is a group and needs no `type`.
|
|
32
|
-
*/
|
|
33
|
-
fields?: FieldDef[];
|
|
34
|
-
/**
|
|
35
|
-
* Inter-field dependency: this field's rule/format depends on another field's value
|
|
36
|
-
* (e.g. postcode format `dependsOn: { field: country, value: JP }`). Carried through as metadata
|
|
37
|
-
* so the scenario weave + QA honor the relationship — the generator does not resolve the runtime
|
|
38
|
-
* condition (that is scenario/@cases logic), it records it.
|
|
39
|
-
*/
|
|
40
|
-
dependsOn?: { field: string; value?: string };
|
|
41
23
|
}
|
|
42
24
|
|
|
43
25
|
export interface FieldMap {
|
|
@@ -61,13 +43,9 @@ export interface ExpandedPoint {
|
|
|
61
43
|
|
|
62
44
|
export interface ExpandedField {
|
|
63
45
|
name: string;
|
|
64
|
-
type
|
|
46
|
+
type: string;
|
|
65
47
|
blindSpot: boolean;
|
|
66
48
|
points: ExpandedPoint[];
|
|
67
|
-
/** Nested sub-fields (a group field). When present, `points` is empty and children hold the data. */
|
|
68
|
-
children?: ExpandedField[];
|
|
69
|
-
/** Recorded inter-field dependency (metadata only — not resolved deterministically). */
|
|
70
|
-
dependsOn?: { field: string; value?: string };
|
|
71
49
|
}
|
|
72
50
|
|
|
73
51
|
/** Deterministic filler: a string of exactly `n` chars from a charset seed. */
|
|
@@ -94,14 +72,14 @@ function synthesize(dp: DataPoint, type: string, c: Record<string, any>): { valu
|
|
|
94
72
|
if (dp.value != null) return { value: dp.value };
|
|
95
73
|
const hint = `${dp.gen || ''} ${dp.note || ''}`.toLowerCase();
|
|
96
74
|
|
|
97
|
-
// Email length boundaries. `>` / "
|
|
75
|
+
// Email length boundaries. `>` / "vượt" → over-boundary (invalid, offset +1).
|
|
98
76
|
if (type === 'email') {
|
|
99
|
-
const over = hint.includes('>') || hint.includes('over');
|
|
77
|
+
const over = hint.includes('>') || hint.includes('vượt') || hint.includes('over');
|
|
100
78
|
const offset = over ? 1 : hint.includes('-1') ? -1 : hint.includes('+1') ? 1 : 0;
|
|
101
79
|
if (hint.includes('maxlocal') || (hint.includes('local') && (hint.includes('max') || over))) {
|
|
102
80
|
const v = synthEmail(c, 'maxLocal', offset); return v ? { value: v } : { value: null, todo: 'maxLocal' };
|
|
103
81
|
}
|
|
104
|
-
if (hint.includes('maxtotal') || hint.includes('total')) {
|
|
82
|
+
if (hint.includes('maxtotal') || (hint.includes('tổng') && (hint.includes('max') || over)) || (hint.includes('total'))) {
|
|
105
83
|
const v = synthEmail(c, 'maxTotal', offset); return v ? { value: v } : { value: null, todo: 'maxTotal' };
|
|
106
84
|
}
|
|
107
85
|
}
|
|
@@ -148,53 +126,23 @@ function resolveError(dp: DataPoint, errorMap?: Record<string, string>): { expec
|
|
|
148
126
|
return { expectedError: dp.errorRef, open: true }; // placeholder survives; flagged open to confirm
|
|
149
127
|
}
|
|
150
128
|
|
|
151
|
-
/**
|
|
152
|
-
* Make a valid value fresh-per-run by injecting a `{{$timestamp}}` token (resolved at runtime by the
|
|
153
|
-
* TestDataLoader's resolveDynamicVars). Email keeps validity via a `+tag` in the local part; other
|
|
154
|
-
* types append the token. Deterministic at compile time (same token string) — uniqueness is at run time.
|
|
155
|
-
*/
|
|
156
|
-
function makeUnique(value: string, type: string): string {
|
|
157
|
-
if (value.includes('{{$')) return value; // already dynamic
|
|
158
|
-
if (type === 'email' && value.includes('@')) {
|
|
159
|
-
const at = value.indexOf('@');
|
|
160
|
-
return `${value.slice(0, at)}+{{$timestamp}}${value.slice(at)}`;
|
|
161
|
-
}
|
|
162
|
-
return `${value}{{$timestamp}}`;
|
|
163
|
-
}
|
|
164
|
-
|
|
165
129
|
/** Expand one field into valid/boundary/invalid data points using the effective catalog. */
|
|
166
130
|
export function expandField(field: FieldDef, catalog: Catalog): ExpandedField {
|
|
167
|
-
|
|
168
|
-
if (field.
|
|
169
|
-
return {
|
|
170
|
-
name: field.name,
|
|
171
|
-
type: undefined,
|
|
172
|
-
blindSpot: false,
|
|
173
|
-
points: [],
|
|
174
|
-
children: field.fields.map((sub) => expandField(sub, catalog)),
|
|
175
|
-
...(field.dependsOn ? { dependsOn: field.dependsOn } : {}),
|
|
176
|
-
};
|
|
177
|
-
}
|
|
178
|
-
const ft: FieldType | undefined = field.type ? catalog.fieldTypes[field.type] : undefined;
|
|
179
|
-
if (!ft) return { name: field.name, type: field.type, blindSpot: true, points: [], ...(field.dependsOn ? { dependsOn: field.dependsOn } : {}) };
|
|
131
|
+
const ft: FieldType | undefined = catalog.fieldTypes[field.type];
|
|
132
|
+
if (!ft) return { name: field.name, type: field.type, blindSpot: true, points: [] };
|
|
180
133
|
|
|
181
134
|
const c = { ...(ft.constraints || {}), ...(field.constraints || {}) };
|
|
182
135
|
const points: ExpandedPoint[] = [];
|
|
183
136
|
|
|
184
137
|
const push = (dp: DataPoint, group: ExpandedPoint['group']) => {
|
|
185
138
|
const s = synthesize(dp, field.type, c);
|
|
186
|
-
// Uniqueness: only the VALID (happy-path create) value gets a fresh-per-run suffix. Boundary/invalid
|
|
187
|
-
// stay literal — they test rules, not record creation.
|
|
188
|
-
const value = group === 'valid' && field.unique && s.value != null
|
|
189
|
-
? makeUnique(s.value, field.type)
|
|
190
|
-
: s.value;
|
|
191
139
|
points.push({
|
|
192
140
|
id: dp.id,
|
|
193
141
|
group,
|
|
194
|
-
value,
|
|
142
|
+
value: s.value,
|
|
195
143
|
todo: s.todo,
|
|
196
144
|
reason: dp.reason,
|
|
197
|
-
note:
|
|
145
|
+
note: dp.note,
|
|
198
146
|
...resolveError(dp, field.errorMap),
|
|
199
147
|
});
|
|
200
148
|
};
|
|
@@ -222,7 +170,7 @@ export function expandField(field: FieldDef, catalog: Catalog): ExpandedField {
|
|
|
222
170
|
}
|
|
223
171
|
}
|
|
224
172
|
|
|
225
|
-
return { name: field.name, type: field.type, blindSpot: false, points
|
|
173
|
+
return { name: field.name, type: field.type, blindSpot: false, points };
|
|
226
174
|
}
|
|
227
175
|
|
|
228
176
|
/** Expand a whole field-map. */
|
|
@@ -238,30 +186,21 @@ export function renderTestData(expanded: ExpandedField[]): Record<string, any> {
|
|
|
238
186
|
const out: Record<string, any> = {
|
|
239
187
|
_generated_by: 'sungen data gen — standardized test-data (valid/boundary/invalid + CHK trace). Do not hand-edit values; edit the .fields.yaml or common.overwrite.yaml and re-run.',
|
|
240
188
|
};
|
|
241
|
-
for (const f of expanded)
|
|
242
|
-
|
|
243
|
-
}
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
return { _blind_spot: `type "${f.type}" not in Data Factory catalog — provide qa/data-factory/project/${f.name}.yaml`, ...dep };
|
|
189
|
+
for (const f of expanded) {
|
|
190
|
+
if (f.blindSpot) {
|
|
191
|
+
out[f.name] = { _blind_spot: `type "${f.type}" not in Data Factory catalog — provide qa/data-factory/project/${f.name}.yaml` };
|
|
192
|
+
continue;
|
|
193
|
+
}
|
|
194
|
+
const valid = f.points.filter((p) => p.group === 'valid');
|
|
195
|
+
const boundary = f.points.filter((p) => p.group === 'boundary');
|
|
196
|
+
const invalid = f.points.filter((p) => p.group === 'invalid');
|
|
197
|
+
out[f.name] = {
|
|
198
|
+
valid: valid[0]?.value ?? null,
|
|
199
|
+
boundary: boundary.map((p) => trim({ id: p.id, value: p.value, note: p.note, todo: p.todo })),
|
|
200
|
+
invalid: invalid.map((p) => trim({ id: p.id, value: p.value, expected_error: p.expectedError, open: p.open, reason: p.reason, todo: p.todo })),
|
|
201
|
+
};
|
|
255
202
|
}
|
|
256
|
-
|
|
257
|
-
const boundary = f.points.filter((p) => p.group === 'boundary');
|
|
258
|
-
const invalid = f.points.filter((p) => p.group === 'invalid');
|
|
259
|
-
return {
|
|
260
|
-
...dep,
|
|
261
|
-
valid: valid[0]?.value ?? null,
|
|
262
|
-
boundary: boundary.map((p) => trim({ id: p.id, value: p.value, note: p.note, todo: p.todo })),
|
|
263
|
-
invalid: invalid.map((p) => trim({ id: p.id, value: p.value, expected_error: p.expectedError, open: p.open, reason: p.reason, todo: p.todo })),
|
|
264
|
-
};
|
|
203
|
+
return out;
|
|
265
204
|
}
|
|
266
205
|
|
|
267
206
|
function trim(o: Record<string, any>): Record<string, any> {
|
package/src/index.ts
CHANGED
|
@@ -24,8 +24,6 @@ export type { FieldDef, FieldMap, ExpandedField, ExpandedPoint } from './generat
|
|
|
24
24
|
export { lintFieldMap, summarize } from './lint';
|
|
25
25
|
export type { LintIssue } from './lint';
|
|
26
26
|
export { registerDataFactoryCommand } from './cli';
|
|
27
|
-
export { generateFixture } from './fixture';
|
|
28
|
-
export type { FixtureResult } from './fixture';
|
|
29
27
|
export { dataFactoryCoverageSensor } from './sensor';
|
|
30
28
|
export { dfPreconditionCodegen } from './precondition';
|
|
31
29
|
export type { Catalog, FieldType, SecurityBank, Technique, DataPoint, OverwriteWarning } from './types';
|
package/src/lint.ts
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Data Factory lint — the deterministic coverage oracle from phuong-phap-tao-test-data.md §6.
|
|
3
3
|
*
|
|
4
|
-
* For each field it confirms the "
|
|
4
|
+
* For each field it confirms the "đủ" checklist: ≥1 valid, boundary present when the field has a
|
|
5
5
|
* length/number bound, ≥1 invalid per rule, no unresolved values, error codes mapped (not left as
|
|
6
6
|
* placeholders). It also surfaces blind spots (type not in catalog) and — via the catalog loader —
|
|
7
7
|
* stale `.overwrite` warnings. Pure functions; no side effects.
|
|
8
8
|
*/
|
|
9
9
|
import type { Catalog } from './types';
|
|
10
|
-
import type { FieldMap, ExpandedField
|
|
10
|
+
import type { FieldMap, ExpandedField } from './generator';
|
|
11
11
|
import { expandFieldMap } from './generator';
|
|
12
12
|
|
|
13
13
|
export interface LintIssue {
|
|
@@ -22,22 +22,11 @@ const BOUNDED = new Set(['email', 'string', 'password', 'phone', 'postcode', 'ka
|
|
|
22
22
|
export function lintFieldMap(map: FieldMap, catalog: Catalog): { issues: LintIssue[]; expanded: ExpandedField[] } {
|
|
23
23
|
const expanded = expandFieldMap(map, catalog);
|
|
24
24
|
const issues: LintIssue[] = [];
|
|
25
|
+
const byName = new Map(map.fields.map((f) => [f.name, f]));
|
|
25
26
|
|
|
26
|
-
|
|
27
|
-
const leaves: Array<{ f: ExpandedField; def?: FieldDef; path: string }> = [];
|
|
28
|
-
const walk = (efs: ExpandedField[], defs: FieldDef[], prefix: string) => {
|
|
29
|
-
for (const ef of efs) {
|
|
30
|
-
const def = defs.find((d) => d.name === ef.name);
|
|
31
|
-
const path = prefix ? `${prefix}.${ef.name}` : ef.name;
|
|
32
|
-
if (ef.children) walk(ef.children, def?.fields || [], path);
|
|
33
|
-
else leaves.push({ f: ef, def, path });
|
|
34
|
-
}
|
|
35
|
-
};
|
|
36
|
-
walk(expanded, map.fields, '');
|
|
37
|
-
|
|
38
|
-
for (const { f, def, path } of leaves) {
|
|
27
|
+
for (const f of expanded) {
|
|
39
28
|
if (f.blindSpot) {
|
|
40
|
-
issues.push({ field:
|
|
29
|
+
issues.push({ field: f.name, severity: 'error', code: 'BLIND_SPOT',
|
|
41
30
|
message: `type "${f.type}" is not in the Data Factory catalog — supply qa/data-factory/project/${f.name}.yaml (valid set, invalid/nonexistent values, error code, dependencies)` });
|
|
42
31
|
continue;
|
|
43
32
|
}
|
|
@@ -45,25 +34,26 @@ export function lintFieldMap(map: FieldMap, catalog: Catalog): { issues: LintIss
|
|
|
45
34
|
const boundary = f.points.filter((p) => p.group === 'boundary');
|
|
46
35
|
const invalid = f.points.filter((p) => p.group === 'invalid');
|
|
47
36
|
|
|
48
|
-
if (valid.length === 0) issues.push({ field:
|
|
49
|
-
if (invalid.length === 0) issues.push({ field:
|
|
50
|
-
if (
|
|
51
|
-
issues.push({ field:
|
|
37
|
+
if (valid.length === 0) issues.push({ field: f.name, severity: 'error', code: 'NO_VALID', message: 'no valid representative value' });
|
|
38
|
+
if (invalid.length === 0) issues.push({ field: f.name, severity: 'error', code: 'NO_INVALID', message: 'no invalid value — every rule needs at least one' });
|
|
39
|
+
if (BOUNDED.has(f.type) && boundary.length === 0) {
|
|
40
|
+
issues.push({ field: f.name, severity: 'warn', code: 'NO_BOUNDARY', message: `type "${f.type}" usually has min/max — add boundary values or state why not (phuong-phap §6)` });
|
|
52
41
|
}
|
|
53
42
|
|
|
54
43
|
// Unresolved (todo) values — a boundary/gen the constraints could not synthesize.
|
|
55
44
|
for (const p of f.points) {
|
|
56
|
-
if (p.todo) issues.push({ field:
|
|
45
|
+
if (p.todo) issues.push({ field: f.name, severity: 'warn', code: 'UNRESOLVED',
|
|
57
46
|
message: `${p.id} (${p.group}) needs input: ${p.todo}${p.note ? ` — ${p.note}` : ''}` });
|
|
58
47
|
}
|
|
59
48
|
// Placeholder error codes still unmapped.
|
|
60
49
|
const openErrs = invalid.filter((p) => p.open);
|
|
61
50
|
if (openErrs.length) {
|
|
62
|
-
issues.push({ field:
|
|
51
|
+
issues.push({ field: f.name, severity: 'info', code: 'OPEN_ERRORREF',
|
|
63
52
|
message: `${openErrs.length} invalid case(s) use placeholder error codes (${[...new Set(openErrs.map((p) => p.expectedError))].join(', ')}) — map to real codes via errorMap in the field-map, or confirm with BA/Dev` });
|
|
64
53
|
}
|
|
54
|
+
const def = byName.get(f.name);
|
|
65
55
|
if (def?.required && valid.length === 0) {
|
|
66
|
-
issues.push({ field:
|
|
56
|
+
issues.push({ field: f.name, severity: 'error', code: 'REQUIRED_NO_VALID', message: 'required field with no valid value' });
|
|
67
57
|
}
|
|
68
58
|
}
|
|
69
59
|
return { issues, expanded };
|
package/src/types.ts
CHANGED
|
@@ -9,7 +9,7 @@ export interface DataPoint {
|
|
|
9
9
|
id: string;
|
|
10
10
|
/** Literal value (present for concrete cases). */
|
|
11
11
|
value?: string;
|
|
12
|
-
/** Human instruction to synthesize the value when it depends on project constraints (e.g. "local
|
|
12
|
+
/** Human instruction to synthesize the value when it depends on project constraints (e.g. "local@ đúng maxLocal"). */
|
|
13
13
|
gen?: string;
|
|
14
14
|
/** Why this value is invalid (invalid group only). */
|
|
15
15
|
reason?: string;
|
package/src/fixture.ts
DELETED
|
@@ -1,79 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Data Factory — CSV/file fixture generator (5b).
|
|
3
|
-
*
|
|
4
|
-
* Turns a field-map into a **bulk-import fixture file** (a CSV whose columns are the fields and whose
|
|
5
|
-
* rows are standardized data), for testing CSV/Excel import, bulk upload, etc. — the `CSV / Import /
|
|
6
|
-
* Export` checklist area. Two products:
|
|
7
|
-
* • the VALID fixture — N rows of valid values (should import cleanly). Unique fields get a
|
|
8
|
-
* per-row suffix so rows don't collide.
|
|
9
|
-
* • the INVALID fixture — one row per field's first invalid case (all-valid except that one field),
|
|
10
|
-
* plus a trailing `_violation` column naming the field + reason + expected error — so a row-level
|
|
11
|
-
* import-rejection test knows which row should fail and why.
|
|
12
|
-
*
|
|
13
|
-
* Deterministic: same field-map + catalog → same CSV. Columns are the flattened leaf paths (nested
|
|
14
|
-
* groups → dotted headers, e.g. `address.city`). Reference the file from an `@api` upload (`files:`)
|
|
15
|
-
* or a UI file-input step.
|
|
16
|
-
*/
|
|
17
|
-
import type { Catalog } from './types';
|
|
18
|
-
import { expandFieldMap, type FieldMap, type ExpandedField } from './generator';
|
|
19
|
-
|
|
20
|
-
interface Leaf { path: string; field: ExpandedField }
|
|
21
|
-
|
|
22
|
-
/** Flatten nested groups to leaf fields with dotted-path headers. */
|
|
23
|
-
function leaves(expanded: ExpandedField[], prefix = ''): Leaf[] {
|
|
24
|
-
const out: Leaf[] = [];
|
|
25
|
-
for (const f of expanded) {
|
|
26
|
-
const path = prefix ? `${prefix}.${f.name}` : f.name;
|
|
27
|
-
if (f.children) out.push(...leaves(f.children, path));
|
|
28
|
-
else out.push({ path, field: f });
|
|
29
|
-
}
|
|
30
|
-
return out;
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
const validOf = (f: ExpandedField): string => f.points.find((p) => p.group === 'valid' && p.value != null)?.value ?? '';
|
|
34
|
-
const firstInvalid = (f: ExpandedField) => f.points.find((p) => p.group === 'invalid' && p.value != null);
|
|
35
|
-
|
|
36
|
-
/** RFC-4180-ish CSV cell quoting. */
|
|
37
|
-
function cell(v: string): string {
|
|
38
|
-
return /[",\n]/.test(v) ? `"${v.replace(/"/g, '""')}"` : v;
|
|
39
|
-
}
|
|
40
|
-
function row(cells: string[]): string {
|
|
41
|
-
return cells.map(cell).join(',');
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
export interface FixtureResult {
|
|
45
|
-
valid: string; // CSV text (header + N valid rows)
|
|
46
|
-
invalid?: string; // CSV text (header + _violation + one bad row per field)
|
|
47
|
-
headers: string[];
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
/** Build the CSV fixture(s) from a field-map. */
|
|
51
|
-
export function generateFixture(map: FieldMap, catalog: Catalog, opts: { rows?: number; includeInvalid?: boolean } = {}): FixtureResult {
|
|
52
|
-
const rows = Math.max(1, opts.rows ?? 3);
|
|
53
|
-
const ls = leaves(expandFieldMap(map, catalog));
|
|
54
|
-
const headers = ls.map((l) => l.path);
|
|
55
|
-
|
|
56
|
-
// VALID rows — resolve the valid value per field; a `unique` field's `{{$timestamp}}` token is a
|
|
57
|
-
// runtime var, not valid in a static file, so replace it with the row index to keep rows distinct.
|
|
58
|
-
const validRows: string[] = [];
|
|
59
|
-
for (let i = 1; i <= rows; i++) {
|
|
60
|
-
validRows.push(row(ls.map((l) => validOf(l.field).replace(/\{\{\$timestamp\}\}/g, String(i)))));
|
|
61
|
-
}
|
|
62
|
-
const valid = [row(headers), ...validRows].join('\n') + '\n';
|
|
63
|
-
|
|
64
|
-
let invalid: string | undefined;
|
|
65
|
-
if (opts.includeInvalid) {
|
|
66
|
-
const invHeaders = [...headers, '_violation'];
|
|
67
|
-
const invRows: string[] = [];
|
|
68
|
-
for (const target of ls) {
|
|
69
|
-
const bad = firstInvalid(target.field);
|
|
70
|
-
if (!bad) continue; // no concrete invalid value to seed
|
|
71
|
-
const cells = ls.map((l) => (l === target ? (bad.value as string) : validOf(l.field).replace(/\{\{\$timestamp\}\}/g, '1')));
|
|
72
|
-
const why = `${target.path}: ${bad.reason || 'invalid'}${bad.expectedError ? ` (${bad.expectedError})` : ''}`;
|
|
73
|
-
invRows.push(row([...cells, why]));
|
|
74
|
-
}
|
|
75
|
-
invalid = [row(invHeaders), ...invRows].join('\n') + '\n';
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
return { valid, invalid, headers };
|
|
79
|
-
}
|
package/src/readme.ts
DELETED
|
@@ -1,52 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Drops a `qa/data-factory/README.md` into the user's project so the folder explains itself.
|
|
3
|
-
* Write-if-absent (never clobbers a user-edited README), like a config file.
|
|
4
|
-
*/
|
|
5
|
-
import { existsSync, mkdirSync, writeFileSync } from 'fs';
|
|
6
|
-
import { join } from 'path';
|
|
7
|
-
|
|
8
|
-
const README = `# \`qa/data-factory/\` — Data Factory (your project's test-data knowledge)
|
|
9
|
-
|
|
10
|
-
This folder is your project's **test-data standard**: machine-readable data rules that
|
|
11
|
-
\`sungen data gen\` / \`/sungen:create-data-test\` (and \`/sungen:create-test\` step 5.3) expand into
|
|
12
|
-
standardized test-data — **valid / boundary / invalid** values, each carrying a \`CHK-*\` trace id back
|
|
13
|
-
to the spec. It is a reusable asset that **survives \`sungen update\`** (only the driver's shipped
|
|
14
|
-
catalog is refreshed; nothing here is touched).
|
|
15
|
-
|
|
16
|
-
It is **not** a copy of your spec — it is the structured, reusable data-rules layer the deterministic
|
|
17
|
-
generator needs, complementary to \`requirements/spec.md\` (which is prose spec per screen/flow).
|
|
18
|
-
|
|
19
|
-
## What lives here
|
|
20
|
-
|
|
21
|
-
| File | What it is |
|
|
22
|
-
|---|---|
|
|
23
|
-
| \`<name>.fields.yaml\` | **Field-map** for a screen/flow — one entry per input field: its catalog \`type\`, the project's real \`constraints\`, an \`errorMap\` (catalog placeholder code → your real message code), and optionally \`unique: true\`, nested \`fields:\`, \`dependsOn:\`. Authored from \`requirements/spec.md\`; expanded deterministically. A unit with **no input fields** (navigation/list) has no field-map — that's correct. |
|
|
24
|
-
| \`common.overwrite.yaml\` | **Project-wide overrides** to the shipped catalog — your real length limits, real error codes, banned values, extra invalid cases. Deep-merged over the common (later-wins, keyed by \`type\` + data-point \`id\`). Genuinely new project knowledge the shipped catalog can't have. |
|
|
25
|
-
| \`project/<entity>.yaml\` | **Blind-spot answers** — business enums, cross-entity rules, data models the catalog can't know. You fill these when Data Factory hits a field \`type\` it doesn't have (instead of it guessing). |
|
|
26
|
-
| \`recipes.yaml\` | Seed recipes for the \`@dataFactory:<recipe>\` annotation — named field sets bound into \`{{recipe.*}}\` at runtime. |
|
|
27
|
-
|
|
28
|
-
## Good to know
|
|
29
|
-
|
|
30
|
-
- **\`sungen data gen\` merges** into \`test-data/<name>.yaml\` — it replaces only the field keys it owns
|
|
31
|
-
and preserves everything else (create-test data, flow-namespaced keys, \`@cases\`, overrides).
|
|
32
|
-
- **\`unique: true\`** makes a field's valid value fresh per run (\`{{$timestamp}}\`) so a register
|
|
33
|
-
email/username never collides — no cross-run cache.
|
|
34
|
-
- \`sungen data lint\` checks coverage; \`sungen data fixture\` builds a CSV import fixture under
|
|
35
|
-
\`qa/fixtures/\`.
|
|
36
|
-
- Edit the field-map / \`.overwrite\` and re-run — never hand-edit the generated \`test-data\` values.
|
|
37
|
-
|
|
38
|
-
Full guide: https://sungen.sun-asterisk.vn/docs/guides/data-factory
|
|
39
|
-
`;
|
|
40
|
-
|
|
41
|
-
/** Ensure `qa/data-factory/README.md` exists (write-if-absent). */
|
|
42
|
-
export function ensureDataFactoryReadme(cwd: string): void {
|
|
43
|
-
const dir = join(cwd, 'qa', 'data-factory');
|
|
44
|
-
const path = join(dir, 'README.md');
|
|
45
|
-
if (existsSync(path)) return;
|
|
46
|
-
try {
|
|
47
|
-
mkdirSync(dir, { recursive: true });
|
|
48
|
-
writeFileSync(path, README, 'utf8');
|
|
49
|
-
} catch {
|
|
50
|
-
/* best-effort — a missing README must never fail a data command */
|
|
51
|
-
}
|
|
52
|
-
}
|