Buổi 8: i18n Sync & Security Scan Testing
I. Lý Thuyết (35 phút)
Layer 4: i18n Key Parity Testing
Mục tiêu: Đảm bảo tất cả language files có đầy đủ keys.
Hàm countKeys để đếm nested keys:
typescript
function countKeys(obj: Record<string, any>, prefix = ''): string[] {
const keys: string[] = [];
for (const [key, value] of Object.entries(obj)) {
const fullKey = prefix ? `${prefix}.${key}` : key;
if (typeof value === 'object' && value !== null && !Array.isArray(value)) {
keys.push(...countKeys(value, fullKey));
} else {
keys.push(fullKey);
}
}
return keys;
}Ví dụ: en.json có 2 keys:
task.filter.alltask.status.overdue
Nhưng vi.json chỉ có 0 keys → Lỗi parity.
Layer 5: Security Scan - Hardcoded Secrets
Nguy hiểm: Hardcoded API keys trong source code
javascript
// BAD - src/server.js
const FAKE_API_KEY = 'sk-fake-1234567890abcdef';Quét với git ls-files:
bash
git ls-files | grep -E '\.(js|ts|json)$' | xargs grep -n "API_KEY\s*=\s*['\"].*['\"]"cm-secret-shield: 5 Defense Layers
- Write Guard: IDE plugin blocks paste of secrets
- Pre-Commit: git hooks scan commit diffs
- Repo Scan: CI/CD finds hardcoded patterns
- Deploy Gate: Environment validation before deploy
- Runtime Guard: process.env validation at startup
Case Study: March 2026 Security Incident
Sự cố: SUPABASE_SERVICE_KEY bị leak
| Biến | Lưu Trữ ✅ | Không ✗ |
|---|---|---|
SUPABASE_SERVICE_KEY | Cloudflare Secret | Hardcoded .env |
DATABASE_URL | Hosting provider Secret | .env trong git |
API_TOKEN | CI/CD Secret vars | Source code |
Bài học: Mỗi secret có một nhà "cư trú" đúng. Nếu bạn để secret ở nơi sai, nó sẽ bị leak.
II. Thực Hành (50 phút)
Bug #3: Thiếu i18n Keys
Tình huống: TaskFlow app không hiển thị đúng text Tiếng Việt.
Bước 1: Viết Test i18n Sync
File: test/i18n-sync.test.ts
typescript
import { describe, it, expect } from 'vitest';
import * as fs from 'fs';
import * as path from 'path';
// Hàm đếm nested keys
function countKeys(obj: Record<string, any>, prefix = ''): Set<string> {
const keys = new Set<string>();
for (const [key, value] of Object.entries(obj)) {
const fullKey = prefix ? `${prefix}.${key}` : key;
if (typeof value === 'object' && value !== null && !Array.isArray(value)) {
[...countKeys(value, fullKey)].forEach(k => keys.add(k));
} else {
keys.add(fullKey);
}
}
return keys;
}
describe('i18n Sync - Key Parity', () => {
it('should have same keys in en.json and vi.json', () => {
const enPath = path.join(process.cwd(), 'public/static/i18n/en.json');
const viPath = path.join(process.cwd(), 'public/static/i18n/vi.json');
const enData = JSON.parse(fs.readFileSync(enPath, 'utf-8'));
const viData = JSON.parse(fs.readFileSync(viPath, 'utf-8'));
const enKeys = countKeys(enData);
const viKeys = countKeys(viData);
console.log(`English keys: ${enKeys.size}`);
console.log(`Vietnamese keys: ${viKeys.size}`);
// Tìm missing keys
const missing = new Set([...enKeys].filter(k => !viKeys.has(k)));
const extra = new Set([...viKeys].filter(k => !enKeys.has(k)));
if (missing.size > 0) {
console.error(`Missing in Vietnamese: ${[...missing].join(', ')}`);
}
if (extra.size > 0) {
console.warn(`Extra in Vietnamese: ${[...extra].join(', ')}`);
}
expect(missing.size).toBe(0);
expect(extra.size).toBe(0);
});
});Bước 2: Chạy Test (Expect FAIL)
bash
npm run test:gateOutput:
FAIL test/i18n-sync.test.ts > i18n Sync - Key Parity > should have same keys in en.json and vi.json
Missing in Vietnamese: task.filter.all, task.status.overdue
Expected 0 to be 0Bước 3: Kiểm Tra File
bash
cat public/static/i18n/en.jsonen.json:
json
{
"task": {
"filter": {
"all": "All Tasks"
},
"status": {
"overdue": "Overdue"
}
}
}vi.json (thiếu 2 keys):
json
{
"task": {
"filter": {},
"status": {}
}
}Bước 4: Sửa vi.json
bash
# Cập nhật vi.json
cat > public/static/i18n/vi.json << 'EOF'
{
"task": {
"filter": {
"all": "Tất cả"
},
"status": {
"overdue": "Quá hạn"
}
}
}
EOFBước 5: Chạy Test (Expect PASS)
bash
npm run test:gateOutput:
PASS test/i18n-sync.test.ts (125ms)
English keys: 2
Vietnamese keys: 2Bug #5: Hardcoded Secret Key
Tình huống: API key bị hardcode trong source code.
Bước 1: Viết Test Security Scan
File: test/security-scan.test.ts
typescript
import { describe, it, expect } from 'vitest';
import * as fs from 'fs';
import * as path from 'path';
import { execSync } from 'child_process';
describe('Security Scan - No Hardcoded Secrets', () => {
it('should not have hardcoded API keys in src/', () => {
const srcDir = path.join(process.cwd(), 'src');
// Pattern để detect hardcoded secrets
const secretPatterns = [
/API_KEY\s*=\s*['"][a-zA-Z0-9\-_]+['"]/g,
/FAKE_API_KEY\s*=\s*['"][a-zA-Z0-9\-_]+['"]/g,
/SECRET\s*=\s*['"][^'"]+['"]/g,
];
const violations: string[] = [];
function scanDir(dir: string) {
const files = fs.readdirSync(dir);
for (const file of files) {
const filePath = path.join(dir, file);
const stat = fs.statSync(filePath);
if (stat.isDirectory()) {
scanDir(filePath);
} else if (file.endsWith('.js') || file.endsWith('.ts')) {
const content = fs.readFileSync(filePath, 'utf-8');
for (const pattern of secretPatterns) {
const matches = content.match(pattern);
if (matches) {
violations.push(
`${filePath}: ${matches[0]}`
);
}
}
}
}
}
scanDir(srcDir);
if (violations.length > 0) {
console.error('Hardcoded secrets found:');
violations.forEach(v => console.error(` ${v}`));
}
expect(violations).toHaveLength(0);
});
it('should have .env and .dev.vars in .gitignore', () => {
const gitignorePath = path.join(process.cwd(), '.gitignore');
const gitignore = fs.readFileSync(gitignorePath, 'utf-8');
expect(gitignore).toContain('.env');
expect(gitignore).toContain('.dev.vars');
});
it('should use process.env for API key, not hardcoded value', () => {
const serverPath = path.join(process.cwd(), 'src/server.js');
const content = fs.readFileSync(serverPath, 'utf-8');
// BAD: hardcoded
expect(content).not.toContain("const FAKE_API_KEY = 'sk-fake");
// GOOD: from env
expect(content).toContain('process.env.API_KEY');
});
});Bước 2: Chạy Test (Expect FAIL)
bash
npm run test:gateOutput:
FAIL test/security-scan.test.ts > Security Scan - No Hardcoded Secrets > should not have hardcoded API keys in src/
Hardcoded secrets found:
/path/to/src/server.js: const FAKE_API_KEY = 'sk-fake-1234567890abcdef'
Expected [] to have length 0Bước 3: Kiểm Tra src/server.js
bash
grep -n "FAKE_API_KEY" src/server.jsBước 4: Sửa server.js
TRƯỚC:
javascript
const FAKE_API_KEY = 'sk-fake-1234567890abcdef';
const apiKey = FAKE_API_KEY;SAU:
javascript
const apiKey = process.env.API_KEY || '';
if (!apiKey) {
throw new Error('API_KEY must be set in environment variables');
}Bước 5: Tạo .env.example
bash
cat > .env.example << 'EOF'
# Không commit .env vào git!
API_KEY=sk-your-key-here
DATABASE_URL=sqlite://taskflow.db
EOFBước 6: Kiểm Tra .gitignore
bash
cat .gitignoreĐảm bảo có:
.env
.dev.vars
.env.localBước 7: Chạy Test (Expect PASS)
bash
npm run test:gateOutput:
PASS test/security-scan.test.ts (98ms)
✓ should not have hardcoded API keys in src/
✓ should have .env and .dev.vars in .gitignore
✓ should use process.env for API key, not hardcoded valueIII. Verification Checklist
- [ ]
test/i18n-sync.test.tschạy PASS - [ ]
test/security-scan.test.tschạy PASS - [ ]
vi.jsoncó 2 keys mới:task.filter.allvàtask.status.overdue - [ ]
src/server.jsdùngprocess.env.API_KEY, không hardcode - [ ]
.envvà.dev.varscó trong.gitignore - [ ]
npm run test:gatechạy tất cả 5 test layers: PASS
IV. Key Takeaways
| Layer | Test | Bug |
|---|---|---|
| 4: i18n | countKeys parity | vi.json thiếu 2 keys |
| 5: Security | Hardcoded secret pattern scan | FAKE_API_KEY trong server.js |
Golden Rule: "Mỗi biến có một nhà ở đúng."
- API keys →
process.envhoặc Secret manager - Dev configs →
.env.local(gitignore) - Public configs → JSON file hoặc code