Skip to content

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.all
  • task.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

  1. Write Guard: IDE plugin blocks paste of secrets
  2. Pre-Commit: git hooks scan commit diffs
  3. Repo Scan: CI/CD finds hardcoded patterns
  4. Deploy Gate: Environment validation before deploy
  5. Runtime Guard: process.env validation at startup

Case Study: March 2026 Security Incident

Sự cố: SUPABASE_SERVICE_KEY bị leak

BiếnLưu Trữ ✅Không ✗
SUPABASE_SERVICE_KEYCloudflare SecretHardcoded .env
DATABASE_URLHosting provider Secret.env trong git
API_TOKENCI/CD Secret varsSource 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:gate

Output:

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 0

Bước 3: Kiểm Tra File

bash
cat public/static/i18n/en.json

en.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"
    }
  }
}
EOF

Bước 5: Chạy Test (Expect PASS)

bash
npm run test:gate

Output:

PASS  test/i18n-sync.test.ts (125ms)
English keys: 2
Vietnamese keys: 2

Bug #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:gate

Output:

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 0

Bước 3: Kiểm Tra src/server.js

bash
grep -n "FAKE_API_KEY" src/server.js

Bướ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
EOF

Bước 6: Kiểm Tra .gitignore

bash
cat .gitignore

Đảm bảo có:

.env
.dev.vars
.env.local

Bước 7: Chạy Test (Expect PASS)

bash
npm run test:gate

Output:

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 value

III. Verification Checklist

  • [ ] test/i18n-sync.test.ts chạy PASS
  • [ ] test/security-scan.test.ts chạy PASS
  • [ ] vi.json có 2 keys mới: task.filter.alltask.status.overdue
  • [ ] src/server.js dùng process.env.API_KEY, không hardcode
  • [ ] .env.dev.vars có trong .gitignore
  • [ ] npm run test:gate chạy tất cả 5 test layers: PASS

IV. Key Takeaways

LayerTestBug
4: i18ncountKeys parityvi.json thiếu 2 keys
5: SecurityHardcoded secret pattern scanFAKE_API_KEY trong server.js

Golden Rule: "Mỗi biến có một nhà ở đúng."

  • API keys → process.env hoặc Secret manager
  • Dev configs → .env.local (gitignore)
  • Public configs → JSON file hoặc code

Powered by CodyMaster × VitePress