# Lab: Black Box Testing Techniques - ปฏิบัติการทดสอบกล่องดำ (Library Management v2)

### วัตถุประสงค์ (Objectives)

นิสิตจะได้ฝึก:

1. ประยุกต์ใช้ EP และ BVA ออกแบบ test cases สำหรับ Book Search API
2. สร้าง Decision Table สำหรับ Book Borrowing Rules
3. สร้าง State Transition Diagram และ Test Cases สำหรับ Member Status
4. เขียน test cases อย่างน้อย 30 test cases จากเทคนิคทั้งหมด
5. ใช้เครื่องมือจริง (Jest + Supertest) กับ Library Management v2

---

## ส่วนที่ 1: Book Search API - EP และ BVA (20 นาที)

### Requirements (ข้อกำหนด)

**Book Search API Endpoint:** `GET /api/books/search?q=query`

ระบบค้นหาหนังสือมีหลักเกณฑ์ดังนี้:

```javascript
async search(req, res) {
  const { q } = req.query;

  if (!q) {
    return res.status(400).json({ error: "Search query is required" });
  }

  const books = await Book.search(q);
  res.json(books);
}
```

**Search Criteria - เกณฑ์การค้นหา:**

1. **Search Query (q):**
   - Required: ต้องมีค่า
   - ค้นหาใน: Title, Author, ISBN (fields ทั้ง 3)
   - Case-insensitive (ตัวพิมพ์ใหญ่-เล็กไม่สำคัญ)

2. **Response Format:**
   - Success: Array ของ books objects
   - Error: Object มี error message

3. **Performance:**
   - Return time: < 1 second
   - Max results: No explicit limit

#### Task 1.1: Equivalence Partitioning

**กรอกตารางต่อไปนี้:**

| Partition ID | Input Field | Description            | Type    | Example Values       | Expected Result                        |
| ------------ | ----------- | ---------------------- | ------- | -------------------- | -------------------------------------- |
| P1           | q           | Missing query          | Invalid | (no q parameter)     | Error: "Search query is required"      |
| P2           | q           | Empty string           | Invalid | ""                   | Error: "Search query is required"      |
| P3           | q           | Valid single character | Valid   | "a", "2", "ก"        | Return matching books or empty array   |
| P4           | q           | Valid normal text      | Valid   | "Python", "สตรี"     | Return matching books                  |
| P5           | q           | Valid ISBN search      | Valid   | "978-616-123-456-7"  | Return matching book by ISBN           |
| P6           | q           | Text with spaces       | Valid   | "Python Programming" | Return matching books                  |
| P7           | q           | Text with symbols      | Valid   | "Data/Mining", "C++" | Return matching books or error         |
| P8           | q           | Mixed case             | Valid   | "Python", "PYTHON"   | Return same results (case insensitive) |
| P9           | q           | Very long text (>1000) | Valid   | (1000+ chars)        | Return results or empty array          |
| P10          | q           | Special chars only     | Valid   | "@#$%", "!!!"        | Return empty results or error          |
| P11          | q           | Number only            | Valid   | "2024", "123"        | Return matching books                  |
| P12          | q           | Thai characters        | Valid   | "ไทย", "สมชาย"       | Return matching books                  |

#### Task 1.2: Boundary Value Analysis (การวิเคราะห์ค่าที่ขอบเขต)

| Test Case | Input Field | Value                 | Expected Result                    |
| --------- | ----------- | --------------------- | ---------------------------------- |
| BVA-01    | q           | 0 chars (empty)       | Error: "query required"            |
| BVA-02    | q           | 1 char                | Valid (min)                        |
| BVA-03    | q           | 50 chars              | Valid (mid)                        |
| BVA-04    | q           | 255 chars             | Valid (reasonable max)             |
| BVA-05    | q           | 1000+ chars           | Valid or timeout                   |
| BVA-06    | q           | whitespace only       | ❓ " " (depends on implementation) |
| BVA-07    | q           | SQL injection attempt | 🛡️ "'; DROP TABLE books; --"       |
| BVA-08    | q           | XSS attempt           | 🛡️ "<script>alert('xss')</script>" |
| BVA-09    | q           | Unicode emoji         | "📚", "🔍"                         |
| BVA-10    | q           | URL encoding          | ❓ "%20", "%3D"                    |

#### Task 1.3: เขียน Test Cases (Jest + Supertest)

สร้างไฟล์ `src/__tests__/bookSearch.test.js`:

```javascript
const request = require("supertest");
const app = require("../app");
const db = require("../database");

describe("Book Search API - Equivalence Partitioning Tests", () => {
  // Setup: Login before each test
  let authToken;
  let cookie;

  beforeAll(async () => {
    // Note: In real testing with session, you'd login first
    // For this lab, assume session is handled by supertest automatically
  });

  describe("EP-P1 to P2: Missing/Empty Query Parameter", () => {
    test("EP-P1: Missing query parameter should return error", async () => {
      const response = await request(app).get("/api/books/search").expect(400);

      expect(response.body.error).toContain("Search query is required");
    });

    test("EP-P2: Empty string query should return error", async () => {
      const response = await request(app)
        .get("/api/books/search?q=")
        .expect(400);

      expect(response.body.error).toContain("Search query is required");
    });
  });

  describe("EP-P3 to P4: Valid Text Search", () => {
    test("EP-P3: Single character valid search", async () => {
      const response = await request(app)
        .get("/api/books/search?q=a")
        .expect(200);

      expect(Array.isArray(response.body)).toBe(true);
    });

    test("EP-P4: Valid normal text search", async () => {
      const response = await request(app)
        .get("/api/books/search?q=Python")
        .expect(200);

      expect(Array.isArray(response.body)).toBe(true);
      // If results found, check structure
      if (response.body.length > 0) {
        expect(response.body[0]).toHaveProperty("title");
        expect(response.body[0]).toHaveProperty("author");
      }
    });
  });

  describe("EP-P5: ISBN Search", () => {
    test("EP-P5: Search by ISBN should return matching book", async () => {
      const response = await request(app)
        .get("/api/books/search?q=978-616-123-456-7")
        .expect(200);

      expect(Array.isArray(response.body)).toBe(true);
    });
  });

  describe("EP-P6: Search with Spaces", () => {
    test("EP-P6: Search with spaces in query", async () => {
      const response = await request(app)
        .get("/api/books/search?q=Python%20Programming")
        .expect(200);

      expect(Array.isArray(response.body)).toBe(true);
    });
  });

  describe("EP-P7: Search with Special Characters", () => {
    test("EP-P7: Search with special characters", async () => {
      const response = await request(app).get("/api/books/search?q=C%2B%2B"); // C++

      // Should not crash, either return results or empty array
      expect(response.status).toMatch(/^(200|400)$/);
    });
  });

  describe("EP-P8: Case Insensitive Search", () => {
    test("EP-P8: Uppercase search should match lowercase results", async () => {
      const lowerResponse = await request(app)
        .get("/api/books/search?q=python")
        .expect(200);

      const upperResponse = await request(app)
        .get("/api/books/search?q=PYTHON")
        .expect(200);

      const mixedResponse = await request(app)
        .get("/api/books/search?q=PyThOn")
        .expect(200);

      // Results should be similar (case-insensitive)
      expect(lowerResponse.body.length).toBe(upperResponse.body.length);
      expect(lowerResponse.body.length).toBe(mixedResponse.body.length);
    });
  });

  describe("EP-P11 to P12: Numeric and Thai Searches", () => {
    test("EP-P11: Search for year/ISBN numbers", async () => {
      const response = await request(app)
        .get("/api/books/search?q=2024")
        .expect(200);

      expect(Array.isArray(response.body)).toBe(true);
    });

    test("EP-P12: Search with Thai characters", async () => {
      const response = await request(app)
        .get("/api/books/search?q=ไทย")
        .expect(200);

      expect(Array.isArray(response.body)).toBe(true);
    });
  });
});

describe("Book Search API - Boundary Value Analysis Tests", () => {
  describe("BVA-01 to BVA-05: Query Length Boundaries", () => {
    test("BVA-01: Empty query should fail", async () => {
      const response = await request(app)
        .get("/api/books/search?q=")
        .expect(400);

      expect(response.body.error).toBeDefined();
    });

    test("BVA-02: 1 character query (minimum)", async () => {
      const response = await request(app)
        .get("/api/books/search?q=a")
        .expect(200);

      expect(Array.isArray(response.body)).toBe(true);
    });

    test("BVA-03: 50 character query (mid-range)", async () => {
      const query = "a".repeat(50);
      const response = await request(app)
        .get(`/api/books/search?q=${query}`)
        .expect(200);

      expect(Array.isArray(response.body)).toBe(true);
    });

    test("BVA-04: 255 character query (long)", async () => {
      const query = "a".repeat(255);
      const response = await request(app)
        .get(`/api/books/search?q=${encodeURIComponent(query)}`)
        .expect(200);

      expect(Array.isArray(response.body)).toBe(true);
    });

    test("BVA-05: 1000+ character query (very long)", async () => {
      const query = "a".repeat(1000);
      const response = await request(app).get(
        `/api/books/search?q=${encodeURIComponent(query)}`,
      );

      // Should not crash
      expect([200, 400, 414]).toContain(response.status);
    });
  });

  describe("BVA-06 to BVA-10: Special Inputs", () => {
    test("BVA-06: Whitespace-only query", async () => {
      const response = await request(app).get("/api/books/search?q=%20%20%20");

      // Implementation may vary
      expect([200, 400]).toContain(response.status);
    });

    test("BVA-07: SQL injection attempt", async () => {
      const payload = "'; DROP TABLE books; --";
      const response = await request(app).get(
        `/api/books/search?q=${encodeURIComponent(payload)}`,
      );

      // Should return results or error, NOT execute injection
      expect([200, 400]).toContain(response.status);
      // Database should still exist
      const checkDb = await request(app).get("/api/books").expect(200);
      expect(Array.isArray(checkDb.body)).toBe(true);
    });

    test("BVA-08: XSS attempt", async () => {
      const payload = "<script>alert('xss')</script>";
      const response = await request(app)
        .get(`/api/books/search?q=${encodeURIComponent(payload)}`)
        .expect(200);

      // Should return string in JSON, not execute script
      expect(response.body).toBeDefined();
    });

    test("BVA-09: Unicode emoji search", async () => {
      const response = await request(app).get(
        `/api/books/search?q=${encodeURIComponent("📚")}`,
      );

      expect([200, 400]).toContain(response.status);
    });
  });
});
```

---

## ส่วนที่ 2: Book Borrowing Rules - Decision Table (20 นาที)

### Requirements

**Borrow Book API Endpoint:** `POST /api/borrowings`

```javascript
async borrow(req, res) {
  const { memberId, bookId, borrowDate, dueDate } = req.body;

  if (!memberId || !bookId || !borrowDate || !dueDate) {
    return res.status(400).json({
      error: "Member ID, book ID, borrow date, and due date are required"
    });
  }

  // Check member exists and is active
  const member = await Member.findById(memberId);
  if (!member) {
    return res.status(404).json({ error: "Member not found" });
  }
  if (member.status !== "active") {
    return res.status(400).json({ error: "Member is not active" });
  }

  // Check borrowing limit
  const borrowCount = await Member.getBorrowingCount(memberId);
  if (borrowCount >= member.max_books) {
    return res.status(400).json({
      error: `Member has reached maximum borrowing limit (${member.max_books})`
    });
  }

  // Check book exists and is available
  const book = await Book.findById(bookId);
  if (!book) {
    return res.status(404).json({ error: "Book not found" });
  }
  if (book.available_copies <= 0) {
    return res.status(400).json({ error: "Book is not available" });
  }

  // Create borrowing record
  const result = await Borrowing.create(memberId, bookId, borrowDate, dueDate);
  res.status(201).json({ success: true, borrowing_id: result.lastID });
}
```

**Borrowing Rules - กฎการยืมหนังสือ:**

1. **Member Status - สถานะสมาชิก:**
   - Active: สามารถยืมได้
   - Suspended: ไม่สามารถยืมได้
   - Inactive: ไม่สามารถยืมได้

2. **Borrowing Limit (max_books):**
   - Student: 3 books max
   - Faculty: 5 books max
   - Staff: 5 books max

3. **Book Availability (available_copies):**
   - > 0: สามารถยืมได้
   - = 0: ไม่สามารถยืมได้

#### Task 2.1: สร้าง Decision Table

**เงื่อนไข (Conditions) - ใช้ 8-10 rules สำคัญ:**

| Rule                      | R1  | R2                 | R3              | R4              | R5            | R6                  | R7                 | R8               | R9            | R10 |
| ------------------------- | --- | ------------------ | --------------- | --------------- | ------------- | ------------------- | ------------------ | ---------------- | ------------- | --- |
| **Conditions**            |     |                    |                 |                 |               |                     |                    |                  |               |     |
| Member Status = Active    | T   | T                  | T               | T               | T             | F                   | F                  | T                | T             | T   |
| Member Type = Student     | T   | T                  | T               | F               | T             | T                   | T                  | T                | T             | T   |
| Below Borrowing Limit     | T   | T                  | F               | T               | T             | T                   | T                  | T                | F             | T   |
| Book Available_copies > 0 | T   | F                  | T               | T               | T             | T                   | T                  | F                | T             | T   |
| Max Books = 3             | T   | T                  | T               | T               | F             | T                   | T                  | T                | T             | T   |
| **Actions**               |     |                    |                 |                 |               |                     |                    |                  |               |     |
| Allow Borrow?             |     |                    |                 |                 |               |                     |                    |                  |               |     |
| Error Message             | -   | "Book unavailable" | "Limit reached" | "Faculty max 5" | "Wrong limit" | "Member not active" | "Member suspended" | "No copies left" | "Max reached" | -   |

#### Task 2.2: เขียน Test Cases

สร้างไฟล์ `src/__tests__/bookBorrowing.test.js`:

```javascript
const request = require("supertest");
const app = require("../app");

describe("Book Borrowing API - Decision Table Tests", () => {
  let studentMember;
  let facultyMember;
  let suspendedMember;
  let availableBook;
  let unavailableBook;

  beforeAll(async () => {
    // Setup: Create test data
    // In real scenario, would create via API or direct DB

    studentMember = {
      id: 1,
      status: "active",
      member_type: "student",
      max_books: 3,
      current_borrowed: 0,
    };

    facultyMember = {
      id: 2,
      status: "active",
      member_type: "faculty",
      max_books: 5,
      current_borrowed: 0,
    };

    suspendedMember = {
      id: 3,
      status: "suspended",
      max_books: 3,
    };

    availableBook = {
      id: 101,
      available_copies: 2,
    };

    unavailableBook = {
      id: 102,
      available_copies: 0,
    };
  });

  describe("DT-R1: Active student, below limit, book available", () => {
    test("DT-R1: Should allow borrow", async () => {
      const response = await request(app)
        .post("/api/borrowings")
        .send({
          memberId: studentMember.id,
          bookId: availableBook.id,
          borrowDate: new Date().toISOString(),
          dueDate: new Date(
            Date.now() + 14 * 24 * 60 * 60 * 1000,
          ).toISOString(),
        })
        .expect(201);

      expect(response.body.success).toBe(true);
      expect(response.body.borrowing_id).toBeDefined();
    });
  });

  describe("DT-R2: Book not available", () => {
    test("DT-R2: Should deny borrow - book unavailable", async () => {
      const response = await request(app)
        .post("/api/borrowings")
        .send({
          memberId: studentMember.id,
          bookId: unavailableBook.id,
          borrowDate: new Date().toISOString(),
          dueDate: new Date(
            Date.now() + 14 * 24 * 60 * 60 * 1000,
          ).toISOString(),
        })
        .expect(400);

      expect(response.body.error).toContain("not available");
    });
  });

  describe("DT-R3: Borrowing limit reached", () => {
    test("DT-R3: Should deny borrow - limit reached", async () => {
      // First, simulate member already has 3 borrowed books
      const memberAtLimit = {
        id: 10,
        status: "active",
        max_books: 3,
        current_borrowed: 3,
      };

      const response = await request(app)
        .post("/api/borrowings")
        .send({
          memberId: memberAtLimit.id,
          bookId: availableBook.id,
          borrowDate: new Date().toISOString(),
          dueDate: new Date(
            Date.now() + 14 * 24 * 60 * 60 * 1000,
          ).toISOString(),
        })
        .expect(400);

      expect(response.body.error).toContain("reached maximum");
    });
  });

  describe("DT-R4: Faculty member with different limit", () => {
    test("DT-R4: Faculty can borrow 5 books (vs student 3)", async () => {
      // Faculty should have higher limit
      expect(facultyMember.max_books).toBe(5);
      expect(studentMember.max_books).toBe(3);
    });
  });

  describe("DT-R5: Member not found", () => {
    test("DT-R5: Should return 404 - member not found", async () => {
      const response = await request(app)
        .post("/api/borrowings")
        .send({
          memberId: 99999, // Non-existent
          bookId: availableBook.id,
          borrowDate: new Date().toISOString(),
          dueDate: new Date(
            Date.now() + 14 * 24 * 60 * 60 * 1000,
          ).toISOString(),
        })
        .expect(404);

      expect(response.body.error).toContain("Member not found");
    });
  });

  describe("DT-R6: Member suspended", () => {
    test("DT-R6: Should deny borrow - member not active", async () => {
      const response = await request(app)
        .post("/api/borrowings")
        .send({
          memberId: suspendedMember.id,
          bookId: availableBook.id,
          borrowDate: new Date().toISOString(),
          dueDate: new Date(
            Date.now() + 14 * 24 * 60 * 60 * 1000,
          ).toISOString(),
        })
        .expect(400);

      expect(response.body.error).toContain("not active");
    });
  });

  describe("DT-R7: Book not found", () => {
    test("DT-R7: Should return 404 - book not found", async () => {
      const response = await request(app)
        .post("/api/borrowings")
        .send({
          memberId: studentMember.id,
          bookId: 99999, // Non-existent
          borrowDate: new Date().toISOString(),
          dueDate: new Date(
            Date.now() + 14 * 24 * 60 * 60 * 1000,
          ).toISOString(),
        })
        .expect(404);

      expect(response.body.error).toContain("Book not found");
    });
  });

  describe("DT-R8: Missing required parameters", () => {
    test("DT-R8: Missing borrowDate should fail", async () => {
      const response = await request(app)
        .post("/api/borrowings")
        .send({
          memberId: studentMember.id,
          bookId: availableBook.id,
          dueDate: new Date(
            Date.now() + 14 * 24 * 60 * 60 * 1000,
          ).toISOString(),
        })
        .expect(400);

      expect(response.body.error).toContain("required");
    });

    test("DT-R8b: Missing dueDate should fail", async () => {
      const response = await request(app)
        .post("/api/borrowings")
        .send({
          memberId: studentMember.id,
          bookId: availableBook.id,
          borrowDate: new Date().toISOString(),
        })
        .expect(400);

      expect(response.body.error).toContain("required");
    });
  });

  describe("DT-R9: Date validation", () => {
    test("DT-R9: Invalid date format should fail or handle gracefully", async () => {
      const response = await request(app).post("/api/borrowings").send({
        memberId: studentMember.id,
        bookId: availableBook.id,
        borrowDate: "invalid-date",
        dueDate: "invalid-date",
      });

      // Should either reject or parse
      expect([400, 500]).toContain(response.status);
    });

    test("DT-R9b: Due date before borrow date should fail", async () => {
      const today = new Date();
      const tomorrow = new Date(today.getTime() + 24 * 60 * 60 * 1000);

      const response = await request(app).post("/api/borrowings").send({
        memberId: studentMember.id,
        bookId: availableBook.id,
        borrowDate: tomorrow.toISOString(),
        dueDate: today.toISOString(), // Due before borrow
      });

      // Implementation may or may not validate this
      expect([200, 201, 400]).toContain(response.status);
    });
  });

  describe("DT-R10: Integration - Multiple borrows", () => {
    test("DT-R10: Member can borrow multiple books up to limit", async () => {
      const memberId = 20;
      const book1 = 201;
      const book2 = 202;
      const book3 = 203;

      const now = new Date();
      const dueDate = new Date(now.getTime() + 14 * 24 * 60 * 60 * 1000);

      // First borrow
      const res1 = await request(app).post("/api/borrowings").send({
        memberId,
        bookId: book1,
        borrowDate: now.toISOString(),
        dueDate: dueDate.toISOString(),
      });

      expect([200, 201, 400]).toContain(res1.status);
    });
  });
});
```

---

## ส่วนที่ 3: Member Status - State Transition Testing (20 นาที)

### Requirements

**Member Status States - สถานะของสมาชิก:**

- Active: กำลังใช้งาน (สามารถยืมได้)
- Suspended: ระงับการใช้งาน (ยืมไม่ได้)
- Inactive: ไม่ใช้งาน

**Valid Transitions:**

```
Active → Suspended (admin marks as suspended)
Active → Inactive (member expires or requests)
Suspended → Active (admin lifts suspension)
Suspended → Inactive (automatic after X days)
Inactive → Active (member renews)
```

#### Task 3.1: State Transition Diagram

**วาด/ดูใน draw.io หรือ Mermaid:**

```mermaid
stateDiagram-v2
    [*] --> Active: New Member

    Active --> Suspended: Admin suspends<br/>(overdue > 30 days, late fees unpaid)
    Active --> Inactive: Member requests<br/>or auto-expire

    Suspended --> Active: Admin lifts<br/>suspension
    Suspended --> Inactive: Auto-suspend<br/>after 90 days

    Inactive --> Active: Member renews<br/>membership
    Inactive --> [*]: Member leaves
```

#### Task 3.2: State Transition Table

| From State | Event            | To State  | Conditions              | Action                |
| ---------- | ---------------- | --------- | ----------------------- | --------------------- |
| Active     | Admin Suspension | Suspended | > 30 days overdue       | Mark suspended        |
| Active     | Member Request   | Inactive  | Explicit request        | Deactivate            |
| Active     | Auto-expire      | Inactive  | After 1 year            | Auto-deactivate       |
| Suspended  | Admin Lift       | Active    | Admin action            | Restore active status |
| Suspended  | Auto-expire      | Inactive  | After 90 days suspended | Auto-deactivate       |
| Inactive   | Member Renew     | Active    | Renew request + payment | Reactivate member     |
| Inactive   | Expire           | [*]       | > 2 years inactive      | Archive record        |

#### Task 3.3: เขียน Test Cases

สร้างไฟล์ `src/__tests__/memberStatus.test.js`:

```javascript
const request = require("supertest");
const app = require("../app");

describe("Member Status - State Transition Tests", () => {
  describe("Valid State Transitions", () => {
    test("ST-01: Active → Suspended (admin marks)", async () => {
      const memberId = 1;

      // Get initial status
      const before = await request(app)
        .get(`/api/members/${memberId}`)
        .expect(200);

      expect(before.body.status).toBe("active");

      // Update to suspended
      const response = await request(app)
        .put(`/api/members/${memberId}`)
        .send({
          status: "suspended",
          fullName: before.body.full_name,
          memberType: before.body.member_type,
        })
        .expect(200);

      expect(response.body.success).toBe(true);

      // Verify status changed
      const after = await request(app)
        .get(`/api/members/${memberId}`)
        .expect(200);

      expect(after.body.status).toBe("suspended");
    });

    test("ST-02: Active → Inactive (member request)", async () => {
      const memberId = 2;

      const response = await request(app)
        .put(`/api/members/${memberId}`)
        .send({
          status: "inactive",
          fullName: "Name",
          memberType: "student",
        })
        .expect(200);

      expect(response.body.success).toBe(true);
    });

    test("ST-03: Suspended → Active (admin lifts)", async () => {
      const memberId = 3;

      // First, set to suspended
      await request(app).put(`/api/members/${memberId}`).send({
        status: "suspended",
        fullName: "Name",
        memberType: "student",
      });

      // Then lift suspension
      const response = await request(app)
        .put(`/api/members/${memberId}`)
        .send({
          status: "active",
          fullName: "Name",
          memberType: "student",
        })
        .expect(200);

      expect(response.body.success).toBe(true);
    });

    test("ST-04: Inactive → Active (member renew)", async () => {
      const memberId = 4;

      // First set to inactive
      await request(app).put(`/api/members/${memberId}`).send({
        status: "inactive",
        fullName: "Name",
        memberType: "student",
      });

      // Then renew (back to active)
      const response = await request(app)
        .put(`/api/members/${memberId}`)
        .send({
          status: "active",
          fullName: "Name",
          memberType: "student",
        })
        .expect(200);

      expect(response.body.success).toBe(true);
    });
  });

  describe("Invalid State Transitions", () => {
    test("ST-05: Member cannot borrow when suspended", async () => {
      const memberId = 5;
      const bookId = 101;

      // Set member to suspended
      await request(app).put(`/api/members/${memberId}`).send({
        status: "suspended",
        fullName: "Name",
        memberType: "student",
      });

      // Try to borrow
      const response = await request(app)
        .post("/api/borrowings")
        .send({
          memberId,
          bookId,
          borrowDate: new Date().toISOString(),
          dueDate: new Date(
            Date.now() + 14 * 24 * 60 * 60 * 1000,
          ).toISOString(),
        })
        .expect(400);

      expect(response.body.error).toContain("not active");
    });

    test("ST-06: Member cannot borrow when inactive", async () => {
      const memberId = 6;
      const bookId = 102;

      // Set member to inactive
      await request(app).put(`/api/members/${memberId}`).send({
        status: "inactive",
        fullName: "Name",
        memberType: "student",
      });

      // Try to borrow
      const response = await request(app)
        .post("/api/borrowings")
        .send({
          memberId,
          bookId,
          borrowDate: new Date().toISOString(),
          dueDate: new Date(
            Date.now() + 14 * 24 * 60 * 60 * 1000,
          ).toISOString(),
        })
        .expect(400);

      expect(response.body.error).toContain("not active");
    });
  });

  describe("Borrow State Impact", () => {
    test("ST-07: Cannot delete/modify borrowed book", async () => {
      const bookId = 103;

      // Try to delete book that has been borrowed
      const response = await request(app).delete(`/api/books/${bookId}`);

      // Should fail if book is borrowed
      if (response.status === 400) {
        expect(response.body.error).toContain("borrowed");
      }
    });

    test("ST-08: Member with overdue books status tracking", async () => {
      // This would require tracking overdueBooks count
      const memberId = 8;

      const response = await request(app)
        .get(`/api/members/${memberId}`)
        .expect(200);

      // Should indicate if member has overdue books
      expect(response.body).toHaveProperty("status");
      // Can extend to check if system tracks overdue
      if (response.body.borrowingRecords) {
        expect(Array.isArray(response.body.borrowingRecords)).toBe(true);
      }
    });
  });

  describe("Status Effects on API Access", () => {
    test("ST-09: Active member can access all features", async () => {
      const memberId = 9;

      // Member should be able to:
      // 1. View books
      const books = await request(app).get("/api/books").expect(200);

      expect(Array.isArray(books.body)).toBe(true);

      // 2. Search books
      const search = await request(app)
        .get("/api/books/search?q=Python")
        .expect(200);

      expect(Array.isArray(search.body)).toBe(true);
    });

    test("ST-10: Suspended member can still view (but not borrow)", async () => {
      const memberId = 10;

      // Set to suspended
      await request(app).put(`/api/members/${memberId}`).send({
        status: "suspended",
        fullName: "Name",
        memberType: "student",
      });

      // Can still view books
      const books = await request(app).get("/api/books").expect(200);

      expect(Array.isArray(books.body)).toBe(true);

      // But cannot borrow
      const borrow = await request(app)
        .post("/api/borrowings")
        .send({
          memberId,
          bookId: 101,
          borrowDate: new Date().toISOString(),
          dueDate: new Date(
            Date.now() + 14 * 24 * 60 * 60 * 1000,
          ).toISOString(),
        });

      expect(borrow.status).toBe(400);
    });
  });
});
```

---

## Lab Deliverables

### 1. เอกสาร Test Design (PDF/Word) - เอกสารออกแบบการทดสอบ

- Equivalence Partitioning Table (EP-P1 to P12)
- Boundary Value Analysis Table (BVA-01 to BVA-10)
- Decision Table (DT-R1 to R10)
- State Transition Diagram (Mermaid/image)
- State Transition Table

### 2. Test Cases (JavaScript Files) - ไฟล์ test cases

```
src/__tests__/
├── bookSearch.test.js       (EP + BVA tests) - 20+ tests
├── bookBorrowing.test.js    (Decision Table tests) - 10+ tests
└── memberStatus.test.js     (State Transition tests) - 10+ tests
```

**Total: 40+ test cases**

### 3. Test Execution Results - ผลการรัน tests

```bash
# ที่โฟลเดอร์ Library-Management-v2 รัน:
npm test -- --coverage --verbose

# Expected output:
# PASS  src/__tests__/bookSearch.test.js (20 tests)
# PASS  src/__tests__/bookBorrowing.test.js (10 tests)
# PASS  src/__tests__/memberStatus.test.js (10 tests)
# Tests:       40 passed, 40 total
# Coverage:    70%+ line coverage
```

### 4. สรุปผลการทดสอบ (2-3 หน้า) - Summary Report

**ควรมีเนื้อหา:**

- จำนวน test cases แต่ละเทคนิค
  - EP: X test cases
  - BVA: X test cases
  - DT: X test cases
  - ST: X test cases

- Bugs ที่พบ (ถ้ามี)
  - Bug ID
  - ที่พบจากเทคนิคไหน
  - ความรุนแรง
  - วิธี reproduce

- ปัญหาและอุปสรรค
  - ปัญหากับ API
  - ปัญหากับ test setup
  - ปัญหากับ assertions

- บทเรียนที่ได้
  - เทคนิคไหนมีประสิทธิมากสุด
  - edge cases ที่สำคัญ
  - recommendation

---

## เกณฑ์การประเมิน (Grading Rubric)

| หัวข้อ                        | คะแนน | รายละเอียด                                                               |
| ----------------------------- | ----- | ------------------------------------------------------------------------ |
| **Test Design Documents**     | 30%   | EP Tables (8%), BVA Tables (8%), Decision Table (7%), State Diagram (7%) |
| **Test Cases Implementation** | 50%   | Code quality (20%), Correctness (15%), Documentation (15%)               |
| **Test Execution**            | 10%   | All 40+ tests pass, proper assertions                                    |
| **Report**                    | 10%   | Summary, findings, insights                                              |
| **Total**                     | 100%  |                                                                          |

**หมายเหตุ (Note):**

- Minimum 40 test cases ผ่านทั้งหมด
- Test coverage ≥ 70%
- อธิบาย test case name อย่างชัดเจน (EP-P1, BVA-02 เป็นต้น)

---

## Tips สำหรับนิสิต (Tips for Students)

1. **ทำการ analyze requirements ก่อนเขียน test**
   - อ่าน API documentation และ code
   - เขียน EP และ BVA table ดีๆ ก่อนทำ tests

2. **ใช้ meaningful test case names**
   - `test('should work')`
   - `test('EP-P1: Missing query parameter should return error')`

3. **จำแนก normal vs edge cases**
   - Normal: `q="Python"` → ได้ผลลัพธ์
   - Edge: `q=""` → error, `q` หายไป → error

4. **ทำให้ test cases independent**

   ```javascript
   beforeEach(() => {
     // Setup fresh data for each test
   });
   ```

5. **Test both success and failure paths**
   - Valid input → should succeed
   - Invalid input → should fail with proper error

6. **เขียน clear assertions**

   ```javascript
   // Good
   expect(response.body.error).toContain("required");
   expect(response.status).toBe(400);

   // Avoid
   expect(response).toBeTruthy();
   ```

7. **Use data-driven tests for similar cases**
   ```javascript
   describe.each([...])('EP-P%s: %s', (id, query, expected) => {
     // Runs same test with different data
   });
   ```

---

## How to Run Tests

### Prerequisite

```bash
cd Library-Management-v2
npm install
docker-compose up -d  # Start database
npm test
```

### Run Specific Test File

```bash
npm test -- bookSearch.test.js
npm test -- bookBorrowing.test.js
npm test -- memberStatus.test.js
```

### Run with Coverage

```bash
npm test -- --coverage
npm test -- --coverage --coveragePathIgnorePatterns=node_modules
```

### Watch Mode (Auto rerun on file change)

```bash
npm test -- --watch
```

---

## คำถามท้ายบท (End-of-Lab Questions)

1. **Equivalence Partitioning vs Boundary Value Analysis**
   - EP ใช้สำหรับอะไร？BVA ใช้สำหรับอะไร？
   - ทำไมต้องใช้ทั้งคู่?

2. **Decision Table Dimensions**
   - ถ้ามี 5 conditions โดยแต่ละ 2 value จะมีกี่ rules?
   - วิธี reduce rules ให้น้อยลงมีอะไรบ้าง?

3. **State Transition Testing**
   - แตกต่างจาก Unit Testing อย่างไร?
   - ควรใช้ร่วมกับเทคนิคไหน?

4. **API Testing vs UI Testing**
   - ทำไมใช้ API testing (Supertest) ในบทนี้?
   - ข้อดี-ข้อเสีย เทียบกับ UI testing?

5. **Security Testing**
   - SQL injection, XSS tests เป็นส่วนของ black box testing หรือไม่?
   - ควรทำให้เข้ม ยังไง?

---

## ข้อมูลเพิ่มเติม

- **Library Management v2:** `/Library-Management-v2/`
- **API Docs:** `Library-Management-v2/API_DOCUMENTATION.md`
- **Jest Docs:** https://jestjs.io/docs/getting-started
- **Supertest Docs:** https://github.com/visionmedia/supertest
- **ISTQB Black Box Testing:** https://www.istqb.org/

---

---

---

---

## Supertest คืออะไร? (What is Supertest?)

### นิยาม

**Supertest** เป็น HTTP assertion library สำหรับ Node.js ที่ใช้ทดสอบ API endpoints โดยการส่ง HTTP requests และตรวจสอบ responses

```javascript
const request = require("supertest");
const app = require("../app");

// ทดสอบ API endpoint
test("GET /api/books/search should return 200", async () => {
  const response = await request(app)
    .get("/api/books/search?q=Python")
    .expect(200);

  expect(Array.isArray(response.body)).toBe(true);
});
```

### ส่วนประกอบหลัก

#### 1. **request(app)** - เริ่มต้น request

```javascript
request(app); // ส่ง request ไปยัง Express app
```

#### 2. **HTTP Methods** - เลือกวิธีการ

```javascript
.get(path)      // GET request
.post(path)     // POST request
.put(path)      // PUT request
.delete(path)   // DELETE request
.patch(path)    // PATCH request
```

#### 3. **send(data)** - ส่งข้อมูล

```javascript
.send({
  memberId: 1,
  bookId: 101,
  borrowDate: '2024-01-15'
})  // ส่ง JSON body (สำหรับ POST/PUT)
```

#### 4. **set(field, value)** - ตั้ง headers

```javascript
.set('Content-Type', 'application/json')
.set('Authorization', 'Bearer token123')
.set('Cookie', 'session=abc123')
```

#### 5. **expect()** - ตรวจสอบผลลัพธ์

```javascript
.expect(200)                    // ตรวจสอบ HTTP status code
.expect('Content-Type', /json/) // ตรวจสอบ header
.expect((res) => {              // Custom assertion
  if (res.body.error) throw new Error('Unexpected error');
})
```

### ตัวอย่างการใช้งาน

#### ตัวอย่างที่ 1: GET Request (Simple)

```javascript
test("Search books by title", async () => {
  const response = await request(app)
    .get("/api/books/search?q=Python")
    .expect(200); // ตรวจสอบ status code

  // ตรวจสอบ response body
  expect(Array.isArray(response.body)).toBe(true);
  expect(response.body.length).toBeGreaterThan(0);
  expect(response.body[0]).toHaveProperty("title");
  expect(response.body[0]).toHaveProperty("author");
});
```

#### ตัวอย่างที่ 2: POST Request (With data)

```javascript
test("Create new book borrowing", async () => {
  const response = await request(app)
    .post("/api/borrowings") // POST endpoint
    .send({
      // ส่ง request body
      memberId: 1,
      bookId: 101,
      borrowDate: "2024-01-15",
      dueDate: "2024-01-29",
    })
    .expect(201); // ตรวจสอบ status 201 (Created)

  expect(response.body.success).toBe(true);
  expect(response.body.borrowing_id).toBeDefined();
});
```

#### ตัวอย่างที่ 3: Error Handling

```javascript
test("Should fail when member not found", async () => {
  const response = await request(app)
    .post("/api/borrowings")
    .send({
      memberId: 99999, // Non-existent member
      bookId: 101,
      borrowDate: "2024-01-15",
      dueDate: "2024-01-29",
    })
    .expect(404); // ตรวจสอบ error status

  expect(response.body.error).toContain("Member not found");
});
```

#### ตัวอย่างที่ 4: Chain Multiple Assertions

```javascript
test("Complex borrowing validation", async () => {
  const response = await request(app)
    .post("/api/borrowings")
    .send({
      memberId: 1,
      bookId: 101,
      borrowDate: "2024-01-15",
      dueDate: "2024-01-29",
    })
    .set("Content-Type", "application/json") // Set header
    .expect(201) // Status code
    .expect("Content-Type", /json/) // Response type
    .expect((res) => {
      // Custom check
      if (!res.body.borrowing_id) {
        throw new Error("Missing borrowing_id in response");
      }
    });

  expect(response.body.success).toBe(true);
});
```

### Request/Response Cycle

**Supertest ทำตามขั้นตอน:**

```
1. สร้าง request object
   const request = require('supertest')

2. ระบุ HTTP method + endpoint
   .get('/api/books/search?q=Python')

3. (Optional) ส่ง data
   .send({ memberId: 1, ... })

4. (Optional) ตั้ง headers
   .set('Authorization', 'Bearer ...')

5. ตรวจสอบผลลัพธ์
   .expect(200)

6. ได้ response
   const response = await ...
   expect(response.body).toBeDefined()
```

### Supertest vs Manual Testing

| ด้าน                   | Manual (Postman)      | Supertest (Automated)        |
| ---------------------- | --------------------- | ---------------------------- |
| **ความเร็ว**           | 🐢 ช้า (manual click) | 🚀 เร็ว (automated)          |
| **Repeatability**      | ต้อง click ใหม่       | รัน auto ได้                 |
| **CI/CD Integration**  | ไม่ได้                | ได้ (git push ทำให้รัน test) |
| **Version Control**    | ไม่ได้เก็บขั้นตอน     | เก็บในไฟล์ code              |
| **Documentation**      | ไม่มี                 | Inline comments              |
| **Regression Testing** | ต้องทดสอบใหม่ทั้งหมด  | อัตโนมัติตรวจจับ bugs        |

### ทำไม Supertest เหมาะสำหรับ Black Box Testing

1. **ทดสอบเหมือณผู้ใช้งานจริง**
   - ไม่ได้เรียก function โดยตรง
   - ส่ง HTTP request ผ่าน network

2. **ตรวจสอบ Full Stack**
   - HTTP status codes
   - Response headers
   - Response body (JSON)
   - Database integration

3. **ทดสอบ Business Logic**
   - Validation rules
   - Business constraints
   - Error handling

4. **Integration Testing**
   - API routes
   - Middleware
   - Database queries
   - Authentication/Authorization

### 🛠️ Common Assertions

```javascript
// Status code
.expect(200)
.expect(201)
.expect(400)
.expect(404)

// Headers
.expect('Content-Type', /json/)
.expect('Content-Type', 'application/json')

// Response body checks
expect(response.body).toBeDefined()
expect(response.body.success).toBe(true)
expect(response.body.error).toContain('required')
expect(Array.isArray(response.body)).toBe(true)
expect(response.body.length).toBeGreaterThan(0)
expect(response.body[0]).toHaveProperty('id')
```

### ข้อดี/ข้อเสีย

**ข้อดี:**

- ง่ายต่อการเขียน
- Fluent API (chain methods)
- Full HTTP lifecycle testing
- Integrate กับ Jest/Mocha ได้ดี
- Mock/stub ได้
- Session/Cookie handling

**ข้อเสีย:**

- ต้องรัน server
- ช้ากว่า unit tests
- ต้องขึ้นกับ database state
- ไม่ได้ทดสอบ UI/Frontend

### 📚 Installation & Setup

```bash
# Install dependencies
npm install --save-dev supertest jest

# ตั้งค่า jest.config.js
module.exports = {
  testEnvironment: 'node',
  testTimeout: 10000
};

# รัน tests
npm test
npm test -- --watch
npm test -- --coverage
```

### Supertest ใช้เมื่อไหร่

- API Testing (REST, GraphQL)
- Integration Testing
- Black Box Testing
- End-to-End Testing
- Regression Testing
- UI Testing (ใช้ Playwright/Cypress แทน)
- Performance Testing (ใช้ k6/Apache JMeter แทน)

---
