# 📝 BỘ 5 ĐỀ LUYỆN TẬP TEST ĐẦU VÀO - TESTER FRESHER

> **Mục tiêu:** Luyện tập các kỹ năng cơ bản cho vị trí Tester Fresher  
> **Đối tượng:** Fresher Tester chuẩn bị phỏng vấn hoặc tự luyện tập  
> **Yêu cầu:** Kiến thức cơ bản về Test Case, SQL, API Testing  

---

# ĐỀ SỐ 1: ĐĂNG KÝ TÀI KHOẢN

> ⏰ **Thời gian làm bài: 30 phút**  
> 📊 **Phần 1: Test Case (20 điểm) | Phần 2: SQL (10 điểm)**

---

## 📋 MÔ TẢ TÍNH NĂNG

Trang web thương mại điện tử **ShopVN** có chức năng đăng ký tài khoản mới. Người dùng điền form và nhấn nút **"Đăng ký"**.

### Thông tin form đăng ký:

| Trường | Loại | Yêu cầu |
|--------|------|---------|
| Họ và tên | Text | Bắt buộc, 2–50 ký tự, chỉ chứa chữ cái và khoảng trắng |
| Email | Text | Bắt buộc, đúng định dạng email, chưa tồn tại trong hệ thống |
| Số điện thoại | Text | Bắt buộc, 10 chữ số, bắt đầu bằng 0 |
| Mật khẩu | Password | Bắt buộc, 8–20 ký tự, có ít nhất 1 chữ hoa, 1 số, 1 ký tự đặc biệt |
| Xác nhận mật khẩu | Password | Bắt buộc, phải khớp với mật khẩu |
| Ngày sinh | Date | Bắt buộc, người dùng phải từ 18 tuổi trở lên |
| Giới tính | Radio | Bắt buộc, chọn 1 trong: Nam / Nữ / Khác |

### Kết quả mong đợi:
- ✅ **Thành công:** Tài khoản được tạo, chuyển đến trang chào mừng, gửi email xác nhận
- ❌ **Thất bại:** Hiển thị thông báo lỗi tương ứng ngay dưới trường bị lỗi

---

## 🗄️ BẢNG DỮ LIỆU

```sql
-- Bảng users đã có trong hệ thống
CREATE TABLE users (
    id          INT PRIMARY KEY AUTO_INCREMENT,
    full_name   VARCHAR(100),
    email       VARCHAR(100) UNIQUE,
    phone       VARCHAR(15),
    password    VARCHAR(255),
    birthday    DATE,
    gender      ENUM('male', 'female', 'other'),
    status      ENUM('active', 'inactive', 'banned') DEFAULT 'active',
    created_at  DATETIME DEFAULT CURRENT_TIMESTAMP
);

-- Dữ liệu mẫu đã có trong bảng
INSERT INTO users VALUES
(1, 'Nguyen Van An', 'an.nguyen@gmail.com', '0901234567', 'hashed_pw_1', '1990-05-15', 'male', 'active', '2023-01-10 08:00:00'),
(2, 'Tran Thi Bich', 'bich.tran@yahoo.com', '0912345678', 'hashed_pw_2', '1995-08-20', 'female', 'active', '2023-02-14 09:30:00'),
(3, 'Le Van Cuong', 'cuong.le@hotmail.com', '0923456789', 'hashed_pw_3', '1988-12-01', 'male', 'banned', '2023-03-01 10:00:00');
```

---

## ✏️ YÊU CẦU

### Phần 1 – Viết Test Case (20 điểm)

Viết **tối thiểu 15 test case** cho chức năng đăng ký tài khoản theo bảng mẫu sau:

| TC_ID | Tên Test Case | Điều kiện tiên quyết | Dữ liệu đầu vào | Các bước thực hiện | Kết quả mong đợi | Loại test |
|-------|--------------|---------------------|----------------|-------------------|-----------------|-----------|
| TC_REG_001 | ... | ... | ... | ... | ... | Positive/Negative |

> 💡 **Gợi ý:** Hãy nghĩ đến các nhóm: Trường bắt buộc bỏ trống, Độ dài ký tự (min/max/vượt), Định dạng sai, Dữ liệu đã tồn tại, Trường hợp thành công

### Phần 2 – Câu hỏi SQL (10 điểm)

**Câu 1 (3 điểm):** Viết câu SQL để lấy danh sách tất cả user có trạng thái `active`, sắp xếp theo `created_at` mới nhất trước.

**Câu 2 (3 điểm):** Viết câu SQL để kiểm tra email `test@gmail.com` đã tồn tại trong hệ thống chưa.

**Câu 3 (4 điểm):** Viết câu SQL để đếm số lượng user theo từng `gender`, chỉ tính các user có trạng thái `active`.

---

## ✅ ĐÁP ÁN GỢI Ý

### Phần 1 – Test Case mẫu

| TC_ID | Tên Test Case | Điều kiện tiên quyết | Dữ liệu đầu vào | Các bước thực hiện | Kết quả mong đợi | Loại test |
|-------|--------------|---------------------|----------------|-------------------|-----------------|-----------|
| TC_REG_001 | Đăng ký thành công với dữ liệu hợp lệ | Trang đăng ký đang mở | Tên: "Nguyen Van D", Email: "d.nguyen@gmail.com", Phone: "0934567890", Pass: "Test@1234", Confirm: "Test@1234", Birthday: "01/01/1995", Gender: Nam | 1. Mở trang đăng ký 2. Điền đầy đủ thông tin hợp lệ 3. Nhấn "Đăng ký" | Tài khoản được tạo thành công, chuyển trang chào mừng, nhận email xác nhận | Positive |
| TC_REG_002 | Bỏ trống trường Họ và tên | Trang đăng ký đang mở | Tên: để trống, các trường khác hợp lệ | 1. Mở trang đăng ký 2. Bỏ trống trường Họ và tên 3. Điền đủ các trường còn lại 4. Nhấn "Đăng ký" | Hiển thị lỗi "Họ và tên không được để trống" ngay dưới trường | Negative |
| TC_REG_003 | Bỏ trống Email | Trang đăng ký đang mở | Email: để trống, các trường khác hợp lệ | 1. Bỏ trống Email 2. Nhấn "Đăng ký" | Hiển thị lỗi "Email không được để trống" | Negative |
| TC_REG_004 | Email sai định dạng | Trang đăng ký đang mở | Email: "testgmail.com" (thiếu @) | 1. Nhập email sai định dạng 2. Nhấn "Đăng ký" | Hiển thị lỗi "Email không đúng định dạng" | Negative |
| TC_REG_005 | Email đã tồn tại trong hệ thống | User an.nguyen@gmail.com đã tồn tại | Email: "an.nguyen@gmail.com" | 1. Nhập email đã tồn tại 2. Nhấn "Đăng ký" | Hiển thị lỗi "Email này đã được sử dụng" | Negative |
| TC_REG_006 | Mật khẩu dưới 8 ký tự | Trang đăng ký đang mở | Pass: "Test@1" (6 ký tự) | 1. Nhập mật khẩu ngắn 2. Nhấn "Đăng ký" | Hiển thị lỗi "Mật khẩu phải từ 8 ký tự" | Negative |
| TC_REG_007 | Mật khẩu không có chữ hoa | Trang đăng ký đang mở | Pass: "test@1234" | 1. Nhập pass không có chữ hoa 2. Nhấn "Đăng ký" | Hiển thị lỗi "Mật khẩu phải chứa ít nhất 1 chữ hoa" | Negative |
| TC_REG_008 | Mật khẩu không có ký tự đặc biệt | Trang đăng ký đang mở | Pass: "TestPass1" | 1. Nhập pass không có ký tự đặc biệt 2. Nhấn "Đăng ký" | Hiển thị lỗi "Mật khẩu phải chứa ký tự đặc biệt" | Negative |
| TC_REG_009 | Xác nhận mật khẩu không khớp | Trang đăng ký đang mở | Pass: "Test@1234", Confirm: "Test@5678" | 1. Nhập mật khẩu không khớp 2. Nhấn "Đăng ký" | Hiển thị lỗi "Mật khẩu xác nhận không khớp" | Negative |
| TC_REG_010 | Số điện thoại không đủ 10 số | Trang đăng ký đang mở | Phone: "090123" (6 số) | 1. Nhập SĐT thiếu số 2. Nhấn "Đăng ký" | Hiển thị lỗi "Số điện thoại phải đủ 10 chữ số" | Negative |
| TC_REG_011 | SĐT không bắt đầu bằng 0 | Trang đăng ký đang mở | Phone: "1234567890" | 1. Nhập SĐT không bắt đầu bằng 0 2. Nhấn "Đăng ký" | Hiển thị lỗi "Số điện thoại phải bắt đầu bằng 0" | Negative |
| TC_REG_012 | Người dùng chưa đủ 18 tuổi | Trang đăng ký đang mở | Birthday: ngày sinh cách hiện tại < 18 năm | 1. Nhập ngày sinh chưa đủ 18 tuổi 2. Nhấn "Đăng ký" | Hiển thị lỗi "Bạn phải từ 18 tuổi trở lên" | Negative |
| TC_REG_013 | Tên chứa ký tự số | Trang đăng ký đang mở | Tên: "Nguyen123" | 1. Nhập tên có chứa số 2. Nhấn "Đăng ký" | Hiển thị lỗi "Họ và tên chỉ được chứa chữ cái và khoảng trắng" | Negative |
| TC_REG_014 | Tên vượt quá 50 ký tự | Trang đăng ký đang mở | Tên: 51 ký tự chữ cái | 1. Nhập tên quá dài 2. Nhấn "Đăng ký" | Hiển thị lỗi "Họ và tên không quá 50 ký tự" | Negative |
| TC_REG_015 | Không chọn giới tính | Trang đăng ký đang mở | Giới tính: không chọn | 1. Bỏ qua chọn giới tính 2. Nhấn "Đăng ký" | Hiển thị lỗi "Vui lòng chọn giới tính" | Negative |

### Phần 2 – Đáp án SQL

**Câu 1:**
```sql
-- Lấy danh sách user active, sắp xếp mới nhất trước
SELECT *
FROM users
WHERE status = 'active'
ORDER BY created_at DESC;

-- Giải thích:
-- WHERE status = 'active'  → lọc chỉ user đang active
-- ORDER BY created_at DESC → sắp xếp theo thời gian tạo, mới nhất lên đầu
```

**Câu 2:**
```sql
-- Cách 1: Dùng COUNT
SELECT COUNT(*) AS email_count
FROM users
WHERE email = 'test@gmail.com';
-- Kết quả = 0: chưa tồn tại | Kết quả >= 1: đã tồn tại

-- Cách 2: Dùng EXISTS (tối ưu hơn cho hệ thống lớn)
SELECT EXISTS (
    SELECT 1 FROM users WHERE email = 'test@gmail.com'
) AS is_exist;

-- Giải thích:
-- EXISTS trả về 0 (false) hoặc 1 (true)
-- Tester thường dùng COUNT để dễ đọc kết quả
```

**Câu 3:**
```sql
-- Đếm user theo giới tính, chỉ tính active
SELECT
    gender,
    COUNT(*) AS total_users
FROM users
WHERE status = 'active'
GROUP BY gender;

-- Kết quả mẫu:
-- male   | 1
-- female | 1

-- Giải thích:
-- WHERE lọc active trước
-- GROUP BY gender → gom nhóm theo giới tính
-- COUNT(*) → đếm số dòng trong mỗi nhóm
```

---

---

# ĐỀ SỐ 2: TÌM KIẾM SẢN PHẨM

> ⏰ **Thời gian làm bài: 30 phút**  
> 📊 **Phần 1: Test Case (20 điểm) | Phần 2: SQL (10 điểm)**

---

## 📋 MÔ TẢ TÍNH NĂNG

Trang **ShopVN** có thanh tìm kiếm sản phẩm ở header. Người dùng nhập từ khóa và nhấn **Enter** hoặc click icon 🔍.

### Đặc tả chức năng tìm kiếm:

| Tính năng | Mô tả |
|-----------|-------|
| Tìm theo tên | Tìm kiếm không phân biệt hoa/thường, tìm gần đúng (contains) |
| Bộ lọc danh mục | Dropdown chọn danh mục: Tất cả / Điện tử / Thời trang / Gia dụng |
| Bộ lọc giá | Nhập khoảng giá: Từ [min] đến [max] (đơn vị: VNĐ) |
| Sắp xếp kết quả | Dropdown: Liên quan nhất / Giá thấp→cao / Giá cao→thấp / Mới nhất |
| Phân trang | Hiển thị 10 sản phẩm/trang, có nút Previous/Next |
| Kết quả 0 sản phẩm | Hiển thị thông báo "Không tìm thấy sản phẩm phù hợp" |
| Độ dài từ khóa | Tối đa 100 ký tự |
| Trạng thái sản phẩm | Chỉ hiển thị sản phẩm `status = 'available'` |

---

## 🗄️ BẢNG DỮ LIỆU

```sql
-- Bảng danh mục
CREATE TABLE categories (
    id    INT PRIMARY KEY AUTO_INCREMENT,
    name  VARCHAR(50)
);

INSERT INTO categories VALUES
(1, 'Điện tử'),
(2, 'Thời trang'),
(3, 'Gia dụng');

-- Bảng sản phẩm
CREATE TABLE products (
    id           INT PRIMARY KEY AUTO_INCREMENT,
    name         VARCHAR(200),
    category_id  INT,
    price        DECIMAL(15,2),
    stock        INT DEFAULT 0,
    status       ENUM('available', 'out_of_stock', 'discontinued'),
    created_at   DATETIME DEFAULT CURRENT_TIMESTAMP,
    FOREIGN KEY (category_id) REFERENCES categories(id)
);

INSERT INTO products VALUES
(1,  'iPhone 15 Pro Max',           1, 33990000, 50,  'available',    '2024-01-10 08:00:00'),
(2,  'Samsung Galaxy S24',          1, 22990000, 30,  'available',    '2024-01-15 09:00:00'),
(3,  'Áo thun nam basic',           2,   150000, 200, 'available',    '2024-01-20 10:00:00'),
(4,  'Quần jean nữ slim fit',       2,   350000, 150, 'available',    '2024-02-01 11:00:00'),
(5,  'Nồi cơm điện Panasonic 1.8L', 3,   890000, 80,  'available',    '2024-02-05 12:00:00'),
(6,  'Nokia 3310 (2017)',           1,   750000, 0,   'out_of_stock', '2023-06-01 08:00:00'),
(7,  'Máy lọc không khí Xiaomi',   3,  2490000, 25,  'available',    '2024-02-10 13:00:00'),
(8,  'Áo khoác da nữ',             2,   890000, 60,  'discontinued', '2023-12-01 08:00:00'),
(9,  'iPhone 14',                  1, 19990000, 40,  'available',    '2024-01-05 07:00:00'),
(10, 'Tai nghe Sony WH-1000XM5',   1,  8990000, 35,  'available',    '2024-02-15 14:00:00');
```

---

## ✏️ YÊU CẦU

### Phần 1 – Viết Test Case (20 điểm)

Viết **tối thiểu 12 test case** bao phủ các tình huống sau:
- Tìm kiếm cơ bản (có kết quả, không có kết quả)
- Bộ lọc danh mục và giá
- Tính năng sắp xếp
- Từ khóa đặc biệt (ký tự đặc biệt, khoảng trắng, quá dài)
- Sản phẩm không hiển thị (out_of_stock, discontinued)

### Phần 2 – Câu hỏi SQL (10 điểm)

**Câu 1 (2 điểm):** Lấy tất cả sản phẩm có trạng thái `available`, sắp xếp theo giá từ thấp đến cao.

**Câu 2 (3 điểm):** Tìm các sản phẩm có tên chứa từ "iphone" (không phân biệt hoa/thường) và có trạng thái `available`.

**Câu 3 (3 điểm):** Lấy sản phẩm thuộc danh mục **Điện tử**, có giá từ **5,000,000 đến 30,000,000** VNĐ.

**Câu 4 (2 điểm):** Đếm tổng số sản phẩm `available` trong từng danh mục (hiển thị tên danh mục).

---

## ✅ ĐÁP ÁN GỢI Ý

### Phần 1 – Test Case mẫu

| TC_ID | Tên Test Case | Dữ liệu đầu vào | Kết quả mong đợi | Loại |
|-------|--------------|----------------|-----------------|------|
| TC_SEARCH_001 | Tìm kiếm từ khóa có kết quả | Từ khóa: "iphone" | Hiển thị iPhone 15 Pro Max, iPhone 14 (2 sản phẩm available) | Positive |
| TC_SEARCH_002 | Tìm kiếm không phân biệt hoa/thường | Từ khóa: "IPHONE" | Cùng kết quả với "iphone" | Positive |
| TC_SEARCH_003 | Tìm kiếm không có kết quả | Từ khóa: "xyzabc123" | Hiển thị "Không tìm thấy sản phẩm phù hợp" | Negative |
| TC_SEARCH_004 | Tìm kiếm sản phẩm out_of_stock | Từ khóa: "Nokia 3310" | Không hiển thị sản phẩm (Nokia đang out_of_stock) | Negative |
| TC_SEARCH_005 | Tìm kiếm sản phẩm discontinued | Từ khóa: "Áo khoác da nữ" | Không hiển thị sản phẩm (đã ngừng kinh doanh) | Negative |
| TC_SEARCH_006 | Bộ lọc theo danh mục Điện tử | Từ khóa: trống, Danh mục: Điện tử | Hiển thị tất cả sản phẩm available thuộc Điện tử | Positive |
| TC_SEARCH_007 | Bộ lọc khoảng giá hợp lệ | Từ khóa: trống, Giá: 200.000 - 1.000.000 | Hiển thị sản phẩm trong khoảng giá này | Positive |
| TC_SEARCH_008 | Giá Min lớn hơn giá Max | Giá Min: 5.000.000, Max: 1.000.000 | Hiển thị thông báo lỗi "Giá từ phải nhỏ hơn giá đến" | Negative |
| TC_SEARCH_009 | Nhập giá âm | Giá Min: -1000 | Hiển thị thông báo lỗi "Giá không được âm" | Negative |
| TC_SEARCH_010 | Sắp xếp giá thấp đến cao | Tất cả sản phẩm, Sort: Giá thấp→cao | Sản phẩm đầu tiên có giá thấp nhất (Áo thun 150.000đ) | Positive |
| TC_SEARCH_011 | Từ khóa chứa ký tự đặc biệt | Từ khóa: "'; DROP TABLE--" | Không hiển thị kết quả, hệ thống hoạt động bình thường (không SQL injection) | Security |
| TC_SEARCH_012 | Từ khóa vượt quá 100 ký tự | Từ khóa: 101 ký tự | Hệ thống cắt bớt hoặc hiển thị lỗi hợp lệ, không bị crash | Negative |
| TC_SEARCH_013 | Từ khóa chỉ có khoảng trắng | Từ khóa: "   " | Hiển thị tất cả sản phẩm available (xử lý như tìm kiếm trống) | Boundary |
| TC_SEARCH_014 | Phân trang khi có nhiều kết quả | Tìm tất cả sản phẩm | Hiển thị tối đa 10 sản phẩm/trang, nút phân trang hoạt động | Positive |

### Phần 2 – Đáp án SQL

**Câu 1:**
```sql
SELECT *
FROM products
WHERE status = 'available'
ORDER BY price ASC;

-- ASC = tăng dần (thấp đến cao), có thể bỏ ASC vì đây là mặc định
-- Kết quả: Áo thun 150k → Quần jean 350k → ... → iPhone 15 Pro Max 33.99M
```

**Câu 2:**
```sql
SELECT *
FROM products
WHERE LOWER(name) LIKE '%iphone%'
  AND status = 'available';

-- LOWER(name): chuyển tên về chữ thường để so sánh
-- LIKE '%iphone%': tìm tên chứa chuỗi "iphone" ở bất kỳ vị trí
-- Kết quả: iPhone 15 Pro Max, iPhone 14
```

**Câu 3:**
```sql
SELECT p.*
FROM products p
JOIN categories c ON p.category_id = c.id
WHERE c.name = 'Điện tử'
  AND p.price BETWEEN 5000000 AND 30000000;

-- BETWEEN 5000000 AND 30000000: bao gồm cả 2 đầu mút
-- Kết quả: Samsung Galaxy S24 (22.99M), iPhone 14 (19.99M), Tai nghe Sony (8.99M)
```

**Câu 4:**
```sql
SELECT
    c.name          AS category_name,
    COUNT(p.id)     AS total_available
FROM categories c
LEFT JOIN products p ON c.id = p.category_id AND p.status = 'available'
GROUP BY c.id, c.name;

-- LEFT JOIN: hiển thị cả danh mục không có sản phẩm (count = 0)
-- Điều kiện status nằm trong ON để không loại danh mục trống
-- Kết quả mẫu: Điện tử: 4, Thời trang: 2, Gia dụng: 2
```

---

---

# ĐỀ SỐ 3: ĐẶT HÀNG

> ⏰ **Thời gian làm bài: 30 phút**  
> 📊 **Phần 1: Test Case (18 điểm) | Phần 2: SQL có JOIN (12 điểm)**

---

## 📋 MÔ TẢ TÍNH NĂNG

Người dùng đã đăng nhập vào **ShopVN** có thể đặt hàng theo quy trình:

```
Giỏ hàng → Xác nhận địa chỉ → Chọn thanh toán → Đặt hàng → Xác nhận thành công
```

### Quy tắc nghiệp vụ:

| Rule | Chi tiết |
|------|---------|
| Tối thiểu đơn hàng | Giỏ hàng phải có ít nhất 1 sản phẩm |
| Số lượng | Số lượng đặt phải > 0 và ≤ số lượng tồn kho (`stock`) |
| Địa chỉ giao hàng | Bắt buộc điền: Họ tên, SĐT, Tỉnh/Thành, Quận/Huyện, Địa chỉ chi tiết |
| Mã giảm giá | Không bắt buộc; nếu nhập phải còn hiệu lực và chưa dùng hết lượt |
| Phương thức thanh toán | COD (tiền mặt khi nhận) hoặc Chuyển khoản ngân hàng |
| Tồn kho | Sau khi đặt hàng thành công, `stock` giảm đúng bằng số lượng đặt |
| Phí ship | Miễn phí với đơn hàng ≥ 500.000đ; dưới 500.000đ phí ship 30.000đ |
| Trạng thái đơn | `pending` → `confirmed` → `shipping` → `delivered` / `cancelled` |

---

## 🗄️ BẢNG DỮ LIỆU

```sql
-- Bảng đơn hàng
CREATE TABLE orders (
    id              INT PRIMARY KEY AUTO_INCREMENT,
    user_id         INT,
    status          ENUM('pending','confirmed','shipping','delivered','cancelled'),
    total_price     DECIMAL(15,2),
    shipping_fee    DECIMAL(15,2) DEFAULT 0,
    coupon_code     VARCHAR(50),
    payment_method  ENUM('cod','bank_transfer'),
    shipping_address TEXT,
    created_at      DATETIME DEFAULT CURRENT_TIMESTAMP,
    FOREIGN KEY (user_id) REFERENCES users(id)
);

-- Bảng chi tiết đơn hàng
CREATE TABLE order_items (
    id          INT PRIMARY KEY AUTO_INCREMENT,
    order_id    INT,
    product_id  INT,
    quantity    INT,
    unit_price  DECIMAL(15,2),
    FOREIGN KEY (order_id)   REFERENCES orders(id),
    FOREIGN KEY (product_id) REFERENCES products(id)
);

-- Bảng mã giảm giá
CREATE TABLE coupons (
    id           INT PRIMARY KEY AUTO_INCREMENT,
    code         VARCHAR(50) UNIQUE,
    discount_pct INT,       -- % giảm giá (ví dụ: 10 = giảm 10%)
    max_uses     INT,       -- số lần dùng tối đa
    used_count   INT DEFAULT 0,
    expired_at   DATETIME
);

-- Dữ liệu mẫu coupons
INSERT INTO coupons VALUES
(1, 'SALE10',  10, 100, 50,  '2025-12-31 23:59:59'),
(2, 'NEWUSER', 20, 50,  50,  '2025-06-30 23:59:59'),
(3, 'EXPIRED', 15, 100, 10,  '2023-12-31 23:59:59'),
(4, 'FULLUSED', 5, 10,  10,  '2025-12-31 23:59:59');
-- EXPIRED: đã hết hạn
-- FULLUSED: used_count = max_uses (hết lượt dùng)
```

---

## ✏️ YÊU CẦU

### Phần 1 – Viết Test Case (18 điểm)

Viết **tối thiểu 12 test case** tập trung vào:
- Luồng đặt hàng thành công
- Số lượng vượt tồn kho
- Mã giảm giá (hợp lệ, hết hạn, hết lượt, không tồn tại)
- Địa chỉ giao hàng thiếu thông tin
- Tính phí ship
- Trạng thái đơn hàng sau khi đặt

### Phần 2 – SQL có JOIN (12 điểm)

**Câu 1 (3 điểm):** Lấy danh sách đơn hàng cùng thông tin người dùng (họ tên, email) của tất cả đơn có trạng thái `pending`.

**Câu 2 (4 điểm):** Lấy chi tiết đơn hàng có `id = 1`: tên sản phẩm, số lượng, đơn giá, thành tiền (quantity × unit_price).

**Câu 3 (5 điểm):** Tính tổng doanh thu (sum of total_price) theo từng tháng trong năm 2024, chỉ tính đơn hàng `delivered`. Sắp xếp theo tháng tăng dần.

---

## ✅ ĐÁP ÁN GỢI Ý

### Phần 1 – Test Case mẫu

| TC_ID | Tên Test Case | Dữ liệu đầu vào | Kết quả mong đợi | Loại |
|-------|--------------|----------------|-----------------|------|
| TC_ORDER_001 | Đặt hàng thành công với COD | 1 sản phẩm có stock, địa chỉ đầy đủ, thanh toán COD | Đơn hàng tạo thành công, status = pending, stock giảm đúng số lượng | Positive |
| TC_ORDER_002 | Đặt hàng với mã giảm giá hợp lệ | Mã "SALE10" (giảm 10%), đơn 500.000đ | Tổng tiền = 500.000 × 90% = 450.000đ, tạo đơn thành công | Positive |
| TC_ORDER_003 | Đặt hàng miễn phí ship | Tổng sản phẩm ≥ 500.000đ | Phí ship = 0đ, tổng = giá sản phẩm | Positive |
| TC_ORDER_004 | Đặt hàng có phí ship | Tổng sản phẩm < 500.000đ | Phí ship = 30.000đ, tổng = giá SP + 30.000đ | Positive |
| TC_ORDER_005 | Số lượng đặt vượt tồn kho | Sản phẩm stock = 5, đặt số lượng = 10 | Hiển thị lỗi "Số lượng sản phẩm không đủ, chỉ còn 5 sản phẩm" | Negative |
| TC_ORDER_006 | Số lượng đặt = 0 | Nhập số lượng = 0 | Hiển thị lỗi "Số lượng phải lớn hơn 0" | Negative |
| TC_ORDER_007 | Mã giảm giá đã hết hạn | Mã "EXPIRED" | Hiển thị lỗi "Mã giảm giá đã hết hạn" | Negative |
| TC_ORDER_008 | Mã giảm giá đã hết lượt dùng | Mã "FULLUSED" | Hiển thị lỗi "Mã giảm giá đã hết lượt sử dụng" | Negative |
| TC_ORDER_009 | Mã giảm giá không tồn tại | Mã "XYZXYZ" | Hiển thị lỗi "Mã giảm giá không hợp lệ" | Negative |
| TC_ORDER_010 | Thiếu địa chỉ giao hàng | Bỏ trống trường "Địa chỉ chi tiết" | Hiển thị lỗi "Vui lòng điền đầy đủ địa chỉ giao hàng" | Negative |
| TC_ORDER_011 | Giỏ hàng trống | Không có sản phẩm nào trong giỏ | Nút "Đặt hàng" bị disabled hoặc hiển thị lỗi "Giỏ hàng trống" | Negative |
| TC_ORDER_012 | Đặt hàng khi chưa đăng nhập | Truy cập trang đặt hàng khi logout | Chuyển hướng đến trang đăng nhập | Negative |
| TC_ORDER_013 | Tổng tiền sau giảm giá + phí ship | Mã SALE10, đơn 400.000đ | Sau giảm 10% = 360.000đ < 500k → phí ship 30k → tổng = 390.000đ | Positive |

### Phần 2 – Đáp án SQL

**Câu 1:**
```sql
SELECT
    o.id            AS order_id,
    o.status,
    o.total_price,
    o.created_at,
    u.full_name,
    u.email
FROM orders o
JOIN users u ON o.user_id = u.id
WHERE o.status = 'pending'
ORDER BY o.created_at DESC;

-- INNER JOIN (hay JOIN): kết hợp dòng từ 2 bảng khi điều kiện ON khớp
-- Chỉ lấy đơn hàng có user_id tồn tại trong bảng users
```

**Câu 2:**
```sql
SELECT
    p.name                              AS product_name,
    oi.quantity,
    oi.unit_price,
    (oi.quantity * oi.unit_price)       AS subtotal
FROM order_items oi
JOIN products p ON oi.product_id = p.id
WHERE oi.order_id = 1;

-- order_items JOIN products để lấy tên sản phẩm
-- Tính subtotal ngay trong câu SELECT bằng phép nhân
```

**Câu 3:**
```sql
SELECT
    MONTH(created_at)       AS thang,
    SUM(total_price)        AS tong_doanh_thu
FROM orders
WHERE status = 'delivered'
  AND YEAR(created_at) = 2024
GROUP BY MONTH(created_at)
ORDER BY thang ASC;

-- MONTH(): trích xuất số tháng từ datetime
-- YEAR(): trích xuất năm, lọc chỉ năm 2024
-- SUM(total_price): tổng doanh thu mỗi tháng
-- GROUP BY MONTH: nhóm theo từng tháng
```

---

---

# ĐỀ SỐ 4: API ĐĂNG NHẬP

> ⏰ **Thời gian làm bài: 30 phút**  
> 📊 **Phần 1: Test Case API (20 điểm) | Phần 2: SQL (10 điểm)**

---

## 📋 MÔ TẢ TÍNH NĂNG

API đăng nhập cho ứng dụng **ShopVN Mobile App**.

### Thông tin API:

```
POST /api/v1/auth/login
Content-Type: application/json
```

### Request Body:
```json
{
    "email": "string",
    "password": "string"
}
```

### Response thành công (200 OK):
```json
{
    "status": "success",
    "message": "Đăng nhập thành công",
    "data": {
        "user_id": 1,
        "full_name": "Nguyen Van An",
        "email": "an.nguyen@gmail.com",
        "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
        "token_expires_at": "2024-12-31T23:59:59Z"
    }
}
```

### Các Response lỗi:

| HTTP Status | Mã lỗi | Thông báo | Trường hợp |
|-------------|--------|-----------|------------|
| 400 | ERR_MISSING_FIELD | "Email và mật khẩu không được để trống" | Thiếu email hoặc password |
| 400 | ERR_INVALID_EMAIL | "Email không đúng định dạng" | Email sai format |
| 401 | ERR_INVALID_CREDENTIAL | "Email hoặc mật khẩu không chính xác" | Sai email/password |
| 403 | ERR_ACCOUNT_BANNED | "Tài khoản đã bị khóa, vui lòng liên hệ hỗ trợ" | User bị ban |
| 403 | ERR_ACCOUNT_INACTIVE | "Tài khoản chưa được kích hoạt" | User chưa xác nhận email |
| 429 | ERR_TOO_MANY_REQUESTS | "Quá nhiều lần thử, vui lòng chờ 15 phút" | Nhập sai quá 5 lần liên tiếp |

### Quy tắc bảo mật:
- Sau **5 lần** đăng nhập sai liên tiếp → khóa tạm thời 15 phút
- Token có hiệu lực **24 giờ**
- Password được hash bằng **bcrypt**
- Response không bao giờ trả về password

---

## 🗄️ BẢNG DỮ LIỆU

```sql
-- Dùng lại bảng users từ đề 1
-- Thêm bảng tracking đăng nhập

CREATE TABLE login_attempts (
    id           INT PRIMARY KEY AUTO_INCREMENT,
    email        VARCHAR(100),
    ip_address   VARCHAR(45),
    is_success   BOOLEAN,
    attempted_at DATETIME DEFAULT CURRENT_TIMESTAMP
);

-- Trạng thái users hiện tại:
-- id=1: an.nguyen@gmail.com   | status: active   | pass: "MyPass@123"
-- id=2: bich.tran@yahoo.com   | status: inactive | pass: "HerPass@456"
-- id=3: cuong.le@hotmail.com  | status: banned   | pass: "HisPass@789"
```

---

## ✏️ YÊU CẦU

### Phần 1 – Viết Test Case API (20 điểm)

Viết **tối thiểu 15 test case** cho API đăng nhập. Sử dụng bảng mẫu sau:

| TC_ID | Tên Test Case | Method & URL | Request Body | Expected Status Code | Expected Response | Ghi chú |
|-------|--------------|-------------|-------------|---------------------|------------------|---------|
| TC_API_001 | ... | POST /api/v1/auth/login | {...} | 200 | {...} | ... |

> 💡 **Gợi ý nhóm test case:**  
> - Happy path (đăng nhập thành công)  
> - Validation (thiếu trường, sai định dạng)  
> - Authentication (sai mật khẩu, email không tồn tại)  
> - Authorization (tài khoản bị ban, chưa kích hoạt)  
> - Security (brute force, SQL injection, password trong response)  
> - Edge cases (khoảng trắng, ký tự đặc biệt)

### Phần 2 – SQL (10 điểm)

**Câu 1 (3 điểm):** Lấy 10 lần đăng nhập gần nhất của email `an.nguyen@gmail.com`.

**Câu 2 (3 điểm):** Đếm số lần đăng nhập thất bại của từng email trong **30 phút gần nhất**.

**Câu 3 (4 điểm):** Tìm các email đã đăng nhập thất bại **từ 5 lần trở lên** trong 30 phút gần nhất (cần bị khóa tạm thời).

---

## ✅ ĐÁP ÁN GỢI Ý

### Phần 1 – Test Case API mẫu

| TC_ID | Tên Test Case | Method & URL | Request Body | Expected Status | Expected Response | Ghi chú |
|-------|--------------|-------------|-------------|-----------------|------------------|---------|
| TC_API_001 | Đăng nhập thành công | POST /api/v1/auth/login | `{"email":"an.nguyen@gmail.com","password":"MyPass@123"}` | 200 OK | status:"success", có token, không có password trong response | Positive |
| TC_API_002 | Thiếu trường email | POST /api/v1/auth/login | `{"password":"MyPass@123"}` | 400 | ERR_MISSING_FIELD | Negative |
| TC_API_003 | Thiếu trường password | POST /api/v1/auth/login | `{"email":"an.nguyen@gmail.com"}` | 400 | ERR_MISSING_FIELD | Negative |
| TC_API_004 | Body rỗng | POST /api/v1/auth/login | `{}` | 400 | ERR_MISSING_FIELD | Negative |
| TC_API_005 | Email sai định dạng | POST /api/v1/auth/login | `{"email":"notanemail","password":"MyPass@123"}` | 400 | ERR_INVALID_EMAIL | Negative |
| TC_API_006 | Email không tồn tại | POST /api/v1/auth/login | `{"email":"nobody@gmail.com","password":"MyPass@123"}` | 401 | ERR_INVALID_CREDENTIAL | Security: không tiết lộ email/pass cái nào sai |
| TC_API_007 | Mật khẩu sai | POST /api/v1/auth/login | `{"email":"an.nguyen@gmail.com","password":"WrongPass"}` | 401 | ERR_INVALID_CREDENTIAL | Negative |
| TC_API_008 | Tài khoản bị banned | POST /api/v1/auth/login | `{"email":"cuong.le@hotmail.com","password":"HisPass@789"}` | 403 | ERR_ACCOUNT_BANNED | Negative |
| TC_API_009 | Tài khoản chưa kích hoạt | POST /api/v1/auth/login | `{"email":"bich.tran@yahoo.com","password":"HerPass@456"}` | 403 | ERR_ACCOUNT_INACTIVE | Negative |
| TC_API_010 | Brute force - lần thứ 6 | POST /api/v1/auth/login (lần 6) | Sai password 6 lần liên tiếp | 429 | ERR_TOO_MANY_REQUESTS | Security |
| TC_API_011 | Password trong response | POST /api/v1/auth/login | Đăng nhập thành công | 200 | Response KHÔNG chứa trường "password" | Security |
| TC_API_012 | SQL Injection trong email | POST /api/v1/auth/login | `{"email":"' OR '1'='1","password":"any"}` | 400 hoặc 401 | Hệ thống không bị lỗi, không đăng nhập được | Security |
| TC_API_013 | Email có khoảng trắng đầu/cuối | POST /api/v1/auth/login | `{"email":" an.nguyen@gmail.com ","password":"MyPass@123"}` | 200 | Đăng nhập thành công (hệ thống trim email) hoặc 401 (tùy xử lý) | Edge case |
| TC_API_014 | Token hợp lệ sau đăng nhập | Gọi API có token vừa nhận | Header: Bearer {token} | Token hoạt động trong 24h | Truy cập API protected thành công | Positive |
| TC_API_015 | Request không có Content-Type | POST /api/v1/auth/login | Không set header Content-Type | 400 hoặc 415 | Thông báo lỗi phù hợp | Edge case |

### Phần 2 – Đáp án SQL

**Câu 1:**
```sql
SELECT *
FROM login_attempts
WHERE email = 'an.nguyen@gmail.com'
ORDER BY attempted_at DESC
LIMIT 10;

-- ORDER BY DESC: mới nhất lên đầu
-- LIMIT 10: chỉ lấy 10 dòng
```

**Câu 2:**
```sql
SELECT
    email,
    COUNT(*) AS failed_count
FROM login_attempts
WHERE is_success = FALSE
  AND attempted_at >= NOW() - INTERVAL 30 MINUTE
GROUP BY email
ORDER BY failed_count DESC;

-- NOW() - INTERVAL 30 MINUTE: 30 phút tính từ thời điểm hiện tại
-- is_success = FALSE: chỉ đếm lần thất bại
-- GROUP BY email: đếm theo từng email
```

**Câu 3:**
```sql
SELECT
    email,
    COUNT(*) AS failed_count
FROM login_attempts
WHERE is_success = FALSE
  AND attempted_at >= NOW() - INTERVAL 30 MINUTE
GROUP BY email
HAVING COUNT(*) >= 5
ORDER BY failed_count DESC;

-- HAVING: điều kiện lọc SAU khi đã GROUP BY
-- Khác với WHERE: WHERE lọc trước khi group, HAVING lọc sau khi group
-- HAVING COUNT(*) >= 5: chỉ lấy email bị thất bại từ 5 lần trở lên
```

---

---

# ĐỀ SỐ 5: QUÊN MẬT KHẨU

> ⏰ **Thời gian làm bài: 30 phút**  
> 📊 **Phần 1: Test Case (18 điểm) | Phần 2: Câu hỏi lý thuyết (12 điểm)**

---

## 📋 MÔ TẢ TÍNH NĂNG

Chức năng **Quên mật khẩu** của **ShopVN** hoạt động theo quy trình:

```
[1] Nhập Email
     ↓
[2] Hệ thống gửi email chứa link reset (có hiệu lực 30 phút)
     ↓
[3] Người dùng click link → Mở trang đặt mật khẩu mới
     ↓
[4] Nhập mật khẩu mới + xác nhận mật khẩu mới
     ↓
[5] Đặt lại thành công → Chuyển trang đăng nhập
```

### Chi tiết từng bước:

**Bước 1 – Màn hình nhập email:**
- Trường Email: bắt buộc, đúng định dạng
- Nút **"Gửi link đặt lại"**
- Nếu email **tồn tại** trong hệ thống: gửi email và hiển thị *"Chúng tôi đã gửi link đặt lại mật khẩu đến email của bạn"*
- Nếu email **không tồn tại**: vẫn hiển thị cùng thông báo trên (**không tiết lộ** email có tồn tại hay không)
- Giới hạn gửi: tối đa **3 lần/giờ** cho cùng 1 email

**Bước 3 – Link reset:**
- Link có dạng: `https://shopvn.com/reset-password?token=abc123xyz`
- Token là chuỗi ngẫu nhiên, **chỉ dùng được 1 lần**
- Token hết hiệu lực sau **30 phút**
- Nếu token hết hạn/đã dùng/không hợp lệ: hiển thị thông báo lỗi và link yêu cầu gửi lại

**Bước 4 – Màn hình đặt mật khẩu mới:**
- Mật khẩu mới: 8–20 ký tự, có chữ hoa, có số, có ký tự đặc biệt
- Mật khẩu mới không được trùng với mật khẩu hiện tại
- Xác nhận mật khẩu: phải khớp

---

## 🗄️ BẢNG DỮ LIỆU

```sql
-- Bảng lưu token reset mật khẩu
CREATE TABLE password_reset_tokens (
    id           INT PRIMARY KEY AUTO_INCREMENT,
    user_id      INT,
    token        VARCHAR(255) UNIQUE,
    is_used      BOOLEAN DEFAULT FALSE,
    expired_at   DATETIME,
    created_at   DATETIME DEFAULT CURRENT_TIMESTAMP,
    FOREIGN KEY (user_id) REFERENCES users(id)
);

-- Dữ liệu mẫu
INSERT INTO password_reset_tokens VALUES
(1, 1, 'valid_token_abc123',   FALSE, DATE_ADD(NOW(), INTERVAL 30 MINUTE), NOW()),
(2, 1, 'used_token_def456',    TRUE,  DATE_ADD(NOW(), INTERVAL 30 MINUTE), NOW()),
(3, 2, 'expired_token_ghi789', FALSE, '2023-01-01 00:00:00',               '2023-01-01 00:00:00');
```

---

## ✏️ YÊU CẦU

### Phần 1 – Viết Test Case (18 điểm)

Viết **tối thiểu 14 test case** bao phủ toàn bộ quy trình 5 bước. Đảm bảo có test case cho:
- Bước 1: Email hợp lệ, Email không tồn tại, Email sai định dạng, Giới hạn gửi
- Bước 3: Token hợp lệ, Token hết hạn, Token đã dùng, Token giả mạo
- Bước 4: Đặt mật khẩu mới thành công, Mật khẩu trùng cũ, Xác nhận không khớp, Vi phạm độ phức tạp

### Phần 2 – Câu hỏi lý thuyết (12 điểm)

Trả lời các câu hỏi sau bằng lời giải thích rõ ràng *(không cần viết code, trả lời bằng văn xuôi)*:

**Câu 1 (3 điểm):** Tại sao hệ thống trả về cùng một thông báo *"Chúng tôi đã gửi link đặt lại mật khẩu"* dù email có tồn tại hay không? Đây là loại kiểm tra bảo mật gì?

**Câu 2 (3 điểm):** Token reset mật khẩu cần có những đặc điểm gì để đảm bảo bảo mật? Giải thích tại sao mỗi đặc điểm lại quan trọng.

**Câu 3 (3 điểm):** Nếu bạn là Tester, bạn sẽ kiểm tra gì sau khi người dùng đặt lại mật khẩu thành công? Liệt kê ít nhất 4 điểm cần kiểm tra.

**Câu 4 (3 điểm):** Phân biệt **Verification** và **Validation** trong testing. Cho ví dụ cụ thể từ chức năng quên mật khẩu.

---

## ✅ ĐÁP ÁN GỢI Ý

### Phần 1 – Test Case mẫu

| TC_ID | Tên Test Case | Bước | Dữ liệu đầu vào | Kết quả mong đợi | Loại |
|-------|--------------|------|----------------|-----------------|------|
| TC_FP_001 | Gửi link reset với email hợp lệ, tồn tại | Bước 1 | Email: "an.nguyen@gmail.com" | Hiển thị "Chúng tôi đã gửi link…", email được gửi đến hộp thư | Positive |
| TC_FP_002 | Gửi link reset với email không tồn tại | Bước 1 | Email: "notexist@gmail.com" | Vẫn hiển thị "Chúng tôi đã gửi link…" (không tiết lộ email không tồn tại) | Security |
| TC_FP_003 | Gửi link với email sai định dạng | Bước 1 | Email: "notanemail" | Hiển thị lỗi "Email không đúng định dạng" | Negative |
| TC_FP_004 | Bỏ trống email | Bước 1 | Email: trống | Hiển thị lỗi "Email không được để trống" | Negative |
| TC_FP_005 | Gửi lại quá 3 lần trong 1 giờ | Bước 1 | Gửi lần thứ 4 cùng email | Hiển thị lỗi "Bạn đã gửi quá nhiều lần, vui lòng thử lại sau" | Security |
| TC_FP_006 | Click link reset hợp lệ | Bước 3 | Token: "valid_token_abc123" | Mở trang đặt mật khẩu mới thành công | Positive |
| TC_FP_007 | Click link đã hết hạn | Bước 3 | Token: "expired_token_ghi789" | Hiển thị "Link đã hết hạn, vui lòng yêu cầu gửi lại" | Negative |
| TC_FP_008 | Click link đã sử dụng | Bước 3 | Token: "used_token_def456" | Hiển thị "Link này đã được sử dụng" | Negative |
| TC_FP_009 | Token giả mạo/không hợp lệ | Bước 3 | Token: "hackertoken999" | Hiển thị "Link không hợp lệ" | Security |
| TC_FP_010 | Đặt mật khẩu mới thành công | Bước 4 | New: "NewPass@2024", Confirm: "NewPass@2024" | Thông báo thành công, chuyển trang đăng nhập | Positive |
| TC_FP_011 | Mật khẩu mới trùng mật khẩu cũ | Bước 4 | New: "MyPass@123" (mật khẩu cũ của user) | Hiển thị lỗi "Mật khẩu mới không được trùng với mật khẩu cũ" | Negative |
| TC_FP_012 | Xác nhận mật khẩu không khớp | Bước 4 | New: "NewPass@2024", Confirm: "NewPass@9999" | Hiển thị lỗi "Xác nhận mật khẩu không khớp" | Negative |
| TC_FP_013 | Mật khẩu mới không đủ độ phức tạp | Bước 4 | New: "newpassword" (không có hoa, số, ký tự đặc biệt) | Hiển thị lỗi về yêu cầu mật khẩu | Negative |
| TC_FP_014 | Đăng nhập với mật khẩu cũ sau khi đổi | Toàn bộ | Sau đổi pass thành công, đăng nhập với pass cũ | Đăng nhập thất bại (mật khẩu cũ không còn hiệu lực) | Positive |
| TC_FP_015 | Dùng link reset lần 2 sau khi đã dùng | Bước 3 | Dùng lại token đã dùng ở TC_FP_010 | Hiển thị "Link này đã được sử dụng" (token bị invalidate) | Security |

### Phần 2 – Đáp án câu hỏi lý thuyết

---

**Câu 1 – Tại sao trả về cùng thông báo dù email tồn tại hay không?**

> **Trả lời:**

Đây là kỹ thuật bảo mật gọi là **"User Enumeration Prevention"** (ngăn chặn liệt kê tài khoản).

**Giải thích:** Nếu hệ thống trả về thông báo khác nhau như *"Email không tồn tại"* vs *"Đã gửi link"*, kẻ tấn công có thể viết script tự động thử hàng nghìn email để xác định email nào đã đăng ký trên hệ thống. Thông tin này có thể dùng để:
- Spam email đã xác nhận
- Thực hiện tấn công phishing có chủ đích hơn
- Kết hợp với brute-force password

**Nguyên tắc:** Hệ thống không được tiết lộ thông tin nhạy cảm qua thông báo lỗi — đây thuộc nhóm **Security Testing / Information Disclosure**.

---

**Câu 2 – Token reset mật khẩu cần đặc điểm gì?**

> **Trả lời – 5 đặc điểm quan trọng:**

| Đặc điểm | Tại sao quan trọng |
|----------|-------------------|
| **Ngẫu nhiên, khó đoán** | Token được tạo bằng thuật toán ngẫu nhiên an toàn (cryptographically secure). Nếu dùng số tuần tự (1,2,3...) kẻ tấn công dễ đoán được token của người khác |
| **Có thời hạn ngắn (30 phút)** | Giảm cửa sổ thời gian bị tấn công. Nếu email bị xâm nhập sau 30 phút, link đã hết hiệu lực |
| **Chỉ dùng được 1 lần** | Sau khi dùng xong, token bị invalidate ngay lập tức. Ngăn kẻ tấn công dùng lại link đã chặn được |
| **Đủ dài và phức tạp** | Token ngắn (4-6 ký tự) có thể bị brute-force. Token nên dài ≥ 32 ký tự hex |
| **Không chứa thông tin nhạy cảm** | Token không nên encode user_id hoặc email dưới dạng dễ decode (như base64 đơn giản) |

---

**Câu 3 – Sau khi đặt lại mật khẩu thành công cần kiểm tra gì?**

> **Trả lời – ít nhất 6 điểm cần verify:**

1. ✅ **Đăng nhập với mật khẩu MỚI thành công** — đây là kết quả chính cần đúng
2. ❌ **Đăng nhập với mật khẩu CŨ thất bại** — mật khẩu cũ phải bị vô hiệu hóa hoàn toàn
3. 🔗 **Token đã dùng không dùng được lại** — click lại link cũ phải báo lỗi
4. 📧 **Gửi email thông báo** — người dùng nhận được email thông báo mật khẩu vừa được thay đổi (để họ biết nếu không phải họ thực hiện)
5. 🔒 **Các session cũ bị logout** — nếu user đang đăng nhập ở thiết bị khác, session đó phải bị hủy
6. 🗄️ **Database cập nhật đúng** — kiểm tra bảng `users`: trường `password` đã được cập nhật hash mới; bảng `password_reset_tokens`: `is_used = TRUE`

---

**Câu 4 – Phân biệt Verification và Validation:**

> **Trả lời:**

| | Verification | Validation |
|--|-------------|-----------|
| **Định nghĩa** | Kiểm tra xem sản phẩm/tính năng **được xây dựng đúng theo spec** không? (*"Are we building it right?"*) | Kiểm tra xem sản phẩm/tính năng **đáp ứng đúng nhu cầu người dùng** không? (*"Are we building the right thing?"*) |
| **Thực hiện khi nào** | Trong quá trình phát triển (review tài liệu, code review) | Sau khi có sản phẩm chạy được (testing thực tế) |
| **Phương pháp** | Review, Walkthrough, Inspection | Chạy test case thực tế trên phần mềm |

**Ví dụ từ chức năng Quên mật khẩu:**

- **Verification:** Review tài liệu đặc tả và hỏi: *"Spec có ghi rõ token hết hạn sau 30 phút không? Code review có đúng logic 30 phút không?"* → Kiểm tra tài liệu và code, chưa cần chạy phần mềm.

- **Validation:** Thực sự chạy test case: *tạo token, chờ 31 phút, click link và xem có hiện lỗi không* → Kiểm tra trên phần mềm thực tế xem có đúng với kỳ vọng người dùng không.

---

---

# 📌 BẢNG TỔNG HỢP 5 ĐỀ

| Đề | Chủ đề | Kỹ năng chính | Điểm SQL | Điểm Test Case | Tổng |
|----|--------|--------------|----------|----------------|------|
| 1 | Đăng ký tài khoản | Test Case cơ bản + SQL SELECT/WHERE | 10 | 20 | 30 |
| 2 | Tìm kiếm sản phẩm | Test Case boundary + SQL LIKE/JOIN | 10 | 20 | 30 |
| 3 | Đặt hàng | Test Case nghiệp vụ + SQL JOIN/GROUP | 12 | 18 | 30 |
| 4 | API Đăng nhập | Test Case API + SQL HAVING | 10 | 20 | 30 |
| 5 | Quên mật khẩu | Test Case bảo mật + Lý thuyết | 12 | 18 | 30 |

---

# 💡 LỜI KHUYÊN KHI LÀM BÀI

## ✍️ Khi viết Test Case

```
1. Đọc kỹ MÔ TẢ TÍNH NĂNG trước khi viết (2-3 phút)
2. Phân loại test case theo nhóm:
   ├── Positive: đúng dữ liệu → thành công
   ├── Negative: sai dữ liệu → báo lỗi đúng
   ├── Boundary: giá trị biên (min-1, min, max, max+1)
   └── Security: injection, unauthorized access
3. Viết test case theo thứ tự: Happy path trước → Edge cases sau
4. Kết quả mong đợi phải CỤ THỂ (không viết chung chung "hiển thị lỗi")
```

## 🗄️ Khi viết SQL

```
1. Xác định: cần lấy dữ liệu từ bảng nào?
2. Xác định: có cần kết hợp nhiều bảng không? → dùng JOIN
3. Xác định: có cần lọc điều kiện không? → WHERE (trước group) / HAVING (sau group)
4. Xác định: có cần đếm/tổng hợp không? → GROUP BY + COUNT/SUM
5. Luôn test câu SQL với dữ liệu mẫu đã cho trong đề
```

## ⏱️ Phân bổ thời gian 30 phút

```
├──  2 phút: Đọc đề, hiểu tính năng
├── 15 phút: Viết test case (phần chiếm điểm nhiều nhất)
├── 10 phút: Viết SQL
└──  3 phút: Đọc lại, bổ sung nếu còn thiếu
```

---

> 🎯 **Mục tiêu khi luyện tập:** Không chỉ viết đủ số lượng test case mà hãy đảm bảo mỗi test case đều có **lý do tồn tại** — nó test cái gì, tại sao cần test điều đó.  
> 
> 🔑 **Chìa khóa để pass bài test fresher:** Tư duy có hệ thống + Kết quả mong đợi cụ thể + SQL cơ bản thành thạo