# Week 9 Integration Testing

## Library-Management-v2 - Integration Testing Hands-on

## 🎯 วัตถุประสงค์ Lab

เมื่อจบ Lab นิสิตจะสามารถ:

- นิสิตเขียน Integration Tests สำหรับ API endpoints ด้วย supertest
- นิสิตทำ API Contract Testing ด้วย JSON Schema (Ajv)
- นิสิตจัดการ test data และ database cleanup
- นิสิตทดสอบ error handling และ edge cases
- นิสิตทดสอบ authentication (200, 401, 403, 404)

---

## 📦 Part 1: Setup & Installation

### ⚠️ Important: Authentication Handling

เนื่องจาก Library-Management-v2 มี `requireAuth` middleware ให้เลือกวิธีใดวิธีหนึ่ง:

**Option A: Bypass Auth Middleware** (ง่ายที่สุดสำหรับ Unit/Integration Testing)

```javascript
// src/middleware.js - edit for test environment
if (process.env.NODE_ENV === "test") {
  // Middleware ต้องเช็ก NODE_ENV และ skip auth ใน test mode
}
```

**Option B: Mock Auth Middleware** (ทำให้เหมือน production)

```javascript
// ใช้ jest.mock() เพื่อ mock auth middleware
jest.mock("../middleware/auth", () => ({
  requireAuth: (req, res, next) => next(),
}));
```

**Option C: Test with Valid Token** (หากมี login endpoint)

```javascript
// ดึง token จาก login test แล้วใช้กับ other tests
const token = response.body.token;
await request(app).get("/api/books").set("Authorization", `Bearer ${token}`);
```

**แนะนำ:** ใช้ Option A สำหรับ lab นี้

---

### 1.1 ติดตั้ง Dependencies

```bash
# Navigate to Library-Management-v2 project
cd Library-Management-v2

# Install testing dependencies (if not already installed)
npm install --save-dev ajv ajv-formats

# supertest is already in devDependencies
# Verify installation
npm list supertest ajv
```

### 1.2 Project Structure

โครงสร้าง Library-Management-v2:

```
Library-Management-v2/
├── src/
│   ├── app.js                     # Express app
│   ├── controllers/
│   │   ├── BookController.js      # Books CRUD
│   │   ├── MemberController.js    # Members/Users
│   │   ├── BorrowingController.js # Borrowing operations
│   │   ├── AuthController.js      # Authentication
│   │   └── DashboardController.js
│   ├── routes/
│   │   ├── books.js               # /api/books endpoints
│   │   ├── members.js             # /api/members endpoints
│   │   ├── borrowing.js           # /api/borrowing endpoints
│   │   ├── auth.js                # /api/auth endpoints
│   │   └── dashboard.js
│   ├── middleware/
│   ├── models/
│   ├── database.js
│   └── __tests__/
│       ├── Auth.test.js           # Existing tests
│       └── Books.test.js
├── public/
├── views/
├── package.json
├── jest.config.js
└── .env / .env.example

# Lab: Create new test files
src/__tests__/
  ├── Books.integration.test.js     # ← NEW
  ├── Members.integration.test.js   # ← NEW
  ├── Borrowing.integration.test.js # ← NEW
  ├── contract.test.js              # ← NEW (JSON Schema contract tests)
  └── schemas.js                    # ← NEW (JSON schemas)
```

### 1.3 Current Jest Config

Library-Management-v2 มี `jest.config.js` ดังนี้:

```javascript
// jest.config.js (existing)
module.exports = {
  testEnvironment: "node",
  coveragePathIgnorePatterns: ["/node_modules/", "/database/"],
  testMatch: ["**/__tests__/**/*.test.js", "**/?(*.)+(spec|test).js"],
  collectCoverageFrom: ["src/**/*.js", "!src/app.js", "!src/database.js"],
  verbose: true,
};
```

### 1.4 Create Integration Test Helper File

**สร้าง `src/__tests__/setup.js` (OPTIONAL - สำหรับ cleanup)**

⚠️ **หมายเหตุ:** ไฟล์นี้เป็น **optional** ใช้ถ้านิสิตต้องการ database cleanup อัตโนมัติ

- ถ้าใช้ real database (SQLite connector to actual DB) → ข้าม setup.js
- ถ้าใช้ in-memory database สำหรับ test → ใช้ setup.js นี้

```javascript
// src/__tests__/setup.js
// SQLite setup for integration tests

const sqlite3 = require("sqlite3").verbose();

let testDb;

// Setup test database
beforeAll(async () => {
  testDb = new sqlite3.Database(":memory:"); // In-memory DB for testing

  // Initialize database schema (same as main database)
  testDb.serialize(() => {
    testDb.run(`
      CREATE TABLE books (
        id INTEGER PRIMARY KEY,
        title TEXT NOT NULL,
        author TEXT NOT NULL,
        isbn TEXT UNIQUE,
        publishedYear INTEGER,
        totalCopies INTEGER DEFAULT 1,
        availableCopies INTEGER DEFAULT 1
      )
    `);

    testDb.run(`
      CREATE TABLE members (
        id INTEGER PRIMARY KEY,
        name TEXT NOT NULL,
        email TEXT UNIQUE,
        joinDate TEXT DEFAULT CURRENT_DATE
      )
    `);

    testDb.run(`
      CREATE TABLE borrowing (
        id INTEGER PRIMARY KEY,
        memberId INTEGER,
        bookId INTEGER,
        borrowDate TEXT DEFAULT CURRENT_DATE,
        returnDate TEXT,
        FOREIGN KEY(memberId) REFERENCES members(id),
        FOREIGN KEY(bookId) REFERENCES books(id)
      )
    `);
  });

  global.testDb = testDb;
});

// Cleanup after all tests
afterAll(async () => {
  if (global.testDb) {
    global.testDb.close();
  }
});

// Clean database before each test
beforeEach(async () => {
  if (global.testDb) {
    global.testDb.run("DELETE FROM borrowing");
    global.testDb.run("DELETE FROM books");
    global.testDb.run("DELETE FROM members");
  }
});
```

---

## 📚 Part 2: API Integration Testing ด้วย Supertest

### Exercise 2.1: Book API Integration Testing

สร้าง `src/__tests__/Books.integration.test.js`:

```javascript
const request = require("supertest");
const app = require("../app");

describe("Book API Integration Tests", () => {
  describe("POST /api/books", () => {
    // ✅ Auth middleware has been bypassed (see Part 1 - Authentication Handling)

    test("should create a new book (POST /api/books)", async () => {
      const newBook = {
        title: "The Pragmatic Programmer",
        author: "Andrew Hunt",
        isbn: "978-0201616224",
        publishedYear: 1999,
        totalCopies: 5,
      };

      const response = await request(app)
        .post("/api/books")
        .send(newBook)
        .expect(201); // Expect 201 Created

      // Verify response structure
      expect(response.body).toHaveProperty("id");
      expect(response.body.title).toBe("The Pragmatic Programmer");
      expect(response.body.author).toBe("Andrew Hunt");
    });

    test("should return 400 for invalid book data", async () => {
      const invalidBook = {
        title: "", // Empty title - should fail validation
        author: "Test Author",
      };

      const response = await request(app)
        .post("/api/books")
        .send(invalidBook)
        .expect(400); // Expect 400 Bad Request

      // Verify error response
      expect(response.body).toHaveProperty("error");
    });
  });

  describe("GET /api/books", () => {
    test("should return all books", async () => {
      const response = await request(app).get("/api/books").expect(200); // Expect 200 OK

      expect(Array.isArray(response.body)).toBe(true);
      // Body should be array of books
    });

    test("should search books by keyword", async () => {
      const response = await request(app)
        .get("/api/books/search?q=JavaScript")
        .expect(200); // Expect 200 OK

      expect(Array.isArray(response.body)).toBe(true);
      // Should return filtered books
    });
  });

  describe("GET /api/books/:id", () => {
    test("should return book by id", async () => {
      // Test with a valid book id (assuming id 1 exists)
      const response = await request(app).get("/api/books/1").expect(200); // Expect 200 OK

      expect(response.body).toHaveProperty("id");
      expect(response.body.id).toBe(1);
    });

    test("should return 404 for non-existent book", async () => {
      const response = await request(app).get("/api/books/99999").expect(404); // Expect 404 Not Found

      expect(response.body).toHaveProperty("error");
    });
  });

  describe("PUT /api/books/:id", () => {
    test("should update book data", async () => {
      const updatedData = {
        title: "Updated Title",
        author: "Updated Author",
      };

      const response = await request(app)
        .put("/api/books/1")
        .send(updatedData)
        .expect(200); // Expect 200 OK

      expect(response.body.title).toBe("Updated Title");
      expect(response.body.author).toBe("Updated Author");
    });
  });

  describe("DELETE /api/books/:id", () => {
    test("should delete book", async () => {
      const response = await request(app).delete("/api/books/1").expect(204); // Expect 204 No Content
    });

    test("should return 404 when deleting non-existent book", async () => {
      const response = await request(app)
        .delete("/api/books/99999")
        .expect(404);
    });
  });
});
```

### 🏃 Run Book Tests

```bash
# Run all tests
npm test

# Run only book tests
npm test -- Books.integration.test.js

# Run with coverage
npm test -- --coverage

# Watch mode
npm run test:watch
```

---

## 🔒 Part 3: API Contract Testing

### Exercise 3.1: Define JSON Schemas

สร้าง `src/__tests__/schemas.js`:

```javascript
// src/__tests__/schemas.js

const bookSchema = {
  type: "object",
  properties: {
    id: {
      type: "integer",
      minimum: 1,
    },
    title: {
      type: "string",
      minLength: 1,
      maxLength: 200,
    },
    author: {
      type: "string",
      minLength: 1,
      maxLength: 100,
    },
    isbn: {
      type: "string",
      pattern: "^[0-9-]+$",
      minLength: 10,
      maxLength: 17,
    },
    publishedYear: {
      type: "integer",
      minimum: 1000,
      maximum: 2100,
    },
    totalCopies: {
      type: "integer",
      minimum: 1,
    },
    availableCopies: {
      type: "integer",
      minimum: 0,
    },
    createdAt: {
      type: "string",
      format: "date-time",
    },
    updatedAt: {
      type: "string",
      format: "date-time",
    },
  },
  required: ["id", "title", "author"],
  additionalProperties: false,
};

const bookListSchema = {
  type: "array",
  items: bookSchema,
};

const memberSchema = {
  type: "object",
  properties: {
    id: {
      type: "integer",
      minimum: 1,
    },
    name: {
      type: "string",
      minLength: 1,
      maxLength: 100,
    },
    email: {
      type: "string",
      format: "email",
    },
    joinDate: {
      type: "string",
      format: "date",
    },
    status: {
      type: "string",
      enum: ["active", "suspended", "inactive"],
    },
  },
  required: ["id", "name", "email"],
  additionalProperties: false,
};

const borrowingSchema = {
  type: "object",
  properties: {
    id: {
      type: "integer",
      minimum: 1,
    },
    memberId: {
      type: "integer",
      minimum: 1,
    },
    bookId: {
      type: "integer",
      minimum: 1,
    },
    borrowDate: {
      type: "string",
      format: "date-time",
    },
    returnDate: {
      type: ["string", "null"],
      format: "date-time",
    },
    status: {
      type: "string",
      enum: ["active", "overdue", "returned"],
    },
  },
  required: ["id", "memberId", "bookId", "borrowDate"],
  additionalProperties: false,
};

const errorSchema = {
  type: "object",
  properties: {
    error: {
      type: "string",
      minLength: 1,
    },
    message: {
      type: "string",
    },
    statusCode: {
      type: "integer",
      minimum: 400,
      maximum: 599,
    },
  },
  required: ["error"],
  additionalProperties: true,
};

module.exports = {
  bookSchema,
  bookListSchema,
  memberSchema,
  borrowingSchema,
  errorSchema,
};
```

### Exercise 3.2: Contract Tests

สร้าง `src/__tests__/contract.test.js`:

```javascript
const request = require("supertest");
const Ajv = require("ajv");
const addFormats = require("ajv-formats");
const app = require("../app");
const {
  bookSchema,
  bookListSchema,
  memberSchema,
  borrowingSchema,
  errorSchema,
} = require("./schemas");

// Setup Ajv with formats (email, date-time, etc.)
const ajv = new Ajv({ allErrors: true, strict: false });
addFormats(ajv);

describe("API Contract Tests", () => {
  describe("Book API Contracts", () => {
    let testBookId;

    beforeEach(async () => {
      // Create test book
      const response = await request(app).post("/api/books").send({
        title: "Contract Test Book",
        author: "Test Author",
        isbn: "978-1234567890",
        publishedYear: 2024,
        totalCopies: 5,
      });

      testBookId = response.body.id;
    });

    test("GET /api/books/:id returns valid book schema", async () => {
      const response = await request(app)
        .get(`/api/books/${testBookId}`)
        .expect(200);

      const validate = ajv.compile(bookSchema);
      const valid = validate(response.body);

      if (!valid) {
        console.log("Schema validation errors:", validate.errors);
      }

      expect(valid).toBe(true);
    });

    test("GET /api/books returns valid book list schema", async () => {
      const response = await request(app).get("/api/books").expect(200);

      const validate = ajv.compile(bookListSchema);
      const valid = validate(response.body);

      if (!valid) {
        console.log("Schema validation errors:", validate.errors);
      }

      expect(valid).toBe(true);
    });

    test("POST /api/books returns valid book schema", async () => {
      const response = await request(app)
        .post("/api/books")
        .send({
          title: "New Book",
          author: "New Author",
          isbn: "978-9876543210",
        })
        .expect(201);

      const validate = ajv.compile(bookSchema);
      const valid = validate(response.body);

      expect(valid).toBe(true);
    });

    test("POST /api/books with invalid data returns valid error schema", async () => {
      const response = await request(app)
        .post("/api/books")
        .send({ title: "" }) // Invalid
        .expect(400);

      const validate = ajv.compile(errorSchema);
      const valid = validate(response.body);

      expect(valid).toBe(true);
      expect(response.body).toHaveProperty("error");
    });

    test("Response should not have unexpected fields", async () => {
      const response = await request(app).get(`/api/books/${testBookId}`);

      const allowedFields = [
        "id",
        "title",
        "author",
        "isbn",
        "publishedYear",
        "totalCopies",
        "availableCopies",
        "createdAt",
        "updatedAt",
      ];

      const actualFields = Object.keys(response.body);
      const unexpectedFields = actualFields.filter(
        (field) => !allowedFields.includes(field),
      );

      expect(unexpectedFields).toEqual([]);
    });
  });

  describe("Member API Contracts", () => {
    test("POST /api/members returns valid member schema", async () => {
      const response = await request(app)
        .post("/api/members")
        .send({
          name: "John Doe",
          email: "john@example.com",
        })
        .expect(201); // Expect 201 Created

      const validate = ajv.compile(memberSchema);
      const valid = validate(response.body);

      if (!valid) {
        console.log("Schema validation errors:", validate.errors);
      }

      expect(valid).toBe(true);
    });

    test("GET /api/members/:id returns valid member schema", async () => {
      // Get member with id 1
      const response = await request(app).get("/api/members/1").expect(200); // Expect 200 OK

      const validate = ajv.compile(memberSchema);
      const valid = validate(response.body);
      expect(valid).toBe(true);
    });

    test("Email format validation - invalid email should fail", async () => {
      const response = await request(app)
        .post("/api/members")
        .send({
          name: "Test User",
          email: "invalid-email", // Invalid format
        })
        .expect(400); // Expect 400 Bad Request

      const validate = ajv.compile(errorSchema);
      expect(validate(response.body)).toBe(true);
    });
  });

  describe("Borrowing API Contracts", () => {
    let memberId, bookId;

    beforeEach(async () => {
      // Create member for testing
      memberId = 1; // Assume member with id 1 exists

      // Create book for testing
      bookId = 1; // Assume book with id 1 exists
    });

    test("POST /api/borrowing returns valid borrowing schema", async () => {
      const response = await request(app)
        .post("/api/borrowing")
        .send({
          memberId: memberId,
          bookId: bookId,
        })
        .expect(201); // Expect 201 Created

      const validate = ajv.compile(borrowingSchema);
      const valid = validate(response.body);
      if (!valid) {
        console.log("Schema validation errors:", validate.errors);
      }

      expect(valid).toBe(true);
      expect(response.body.status).toBe("active");
    });

    test("Borrowing status enum validation", async () => {
      const response = await request(app)
        .post("/api/borrowing")
        .send({ memberId, bookId })
        .expect(201); // Expect 201 Created

      // Status must be one of: active, overdue, returned
      expect(["active", "overdue", "returned"]).toContain(response.body.status);
    });

    test("Date format validation", async () => {
      const response = await request(app)
        .post("/api/borrowing")
        .send({ memberId, bookId })
        .expect(201); // Expect 201 Created

      // borrowDate should be in ISO format (YYYY-MM-DD or YYYY-MM-DDTHH:mm:ss)
      expect(response.body.borrowDate).toMatch(/^\d{4}-\d{2}-\d{2}/);
    });

    test("GET /api/borrowing/:id returns valid borrowing schema", async () => {
      const response = await request(app).get("/api/borrowing/1").expect(200); // Expect 200 OK

      const validate = ajv.compile(borrowingSchema);
      const valid = validate(response.body);
      expect(valid).toBe(true);
    });
  });
});
```

### 🏃 Run Contract Tests

```bash
npm test -- contract.test.js
```

---

## � Part 4: Integration Test Best Practices

### Generate Coverage Report

```bash
# Run all tests with coverage
npm test -- --coverage

# Generate HTML coverage report
npm test -- --coverage --coverageReporters=html

# Open coverage report
open coverage/index.html  # Mac
xdg-open coverage/index.html  # Linux
start coverage/index.html  # Windows
```

### Coverage Goals

Integration tests should aim for:

- **Statements**: 70%+
- **Branches**: 65%+
- **Functions**: 70%+
- **Lines**: 70%+

---

## 📋 Lab Checklist สำหรับนิสิต

ให้นิสิตตรวจสอบว่าได้ทำครบทุกข้อแล้ว:

- [ ] ติดตั้ง dependencies (supertest, ajv)
- [ ] สร้าง Book API integration tests (CRUD operations)
- [ ] สร้าง Member API integration tests (CRUD operations)
- [ ] สร้าง Borrowing API integration tests (CRUD operations)
- [ ] ทดสอบ error cases และ edge cases
- [ ] สร้าง JSON schemas สำหรับ Book, Member, Borrowing
- [ ] เขียน contract tests ด้วย Ajv
- [ ] ทดสอบ authentication (expect 200, 401, 403, 404)
- [ ] Generate coverage report
- [ ] ทุก tests ผ่านหมด (green)

---

## 🐛 Common Issues & Solutions

### Issue 1: Database Connection Errors

```
Error: connect ECONNREFUSED 127.0.0.1:5432
```

**Solution:**

```bash
# Make sure test database is running
docker-compose up -d postgres-test

# Or use SQLite for testing
npm install --save-dev sqlite3
```

### Issue 2: Port Already in Use

```
Error: listen EADDRINUSE: address already in use :::3000
```

**Solution:**

```javascript
// In tests, don't start server - use app directly
const app = require("./app"); // Not require('./server')
```

### Issue 3: Tests Failing After Each Other

**Problem:** Tests pass individually but fail when run together

**Solution:**

```javascript
// Clean database before each test
beforeEach(async () => {
  await global.testDb.run("DELETE FROM borrowing");
  await global.testDb.run("DELETE FROM books");
  await global.testDb.run("DELETE FROM members");
});
```

### Issue 4: JSON Schema Validation Fails

**Solution:**

```javascript
// Add formats support
const addFormats = require("ajv-formats");
addFormats(ajv);

// Use strict: false for flexibility
const ajv = new Ajv({ allErrors: true, strict: false });

// Check validation errors
if (!valid) {
  console.log("Errors:", validate.errors);
}
```

---

## 📝 Submission Requirements

### ส่งไฟล์ต่อไปนี้:

1. **Test Files:**
   - `src/__tests__/Books.integration.test.js`
   - `src/__tests__/Members.integration.test.js`
   - `src/__tests__/Borrowing.integration.test.js`
   - `src/__tests__/contract.test.js`

2. **Supporting Files:**
   - `src/__tests__/schemas.js`
   - `src/__tests__/setup.js`

3. **Documentation:**
   - `LAB08_REPORT.md` - Lab report ที่ประกอบด้วย:
     - Test results (screenshots)
     - Coverage report (screenshot)
     - ปัญหาที่พบและวิธีแก้
     - Lessons learned

4. **Screenshots:**
   - All tests passing
   - Coverage report
   - Example of failed test (intentional)

### Grading Criteria (10 points):

- **Tests Implementation** (5 points):
  - Book API tests complete (1.5 pts)
  - Member API tests complete (1.5 pts)
  - Borrowing API tests complete (2 pts)

- **Code Quality** (2 points):
  - Clean, readable code
  - Proper test structure
  - Good assertions

- **Coverage** (2 points):
  - 70%+ statement coverage
  - Edge cases covered

- **Documentation** (1 point):
  - Clear lab report
  - Screenshots included

---

## 🚀 Extra Challenges (Bonus)

หากทำเสร็จเร็ว ลองทำเพิ่ม:

### Challenge 1: Member Management Integration Tests

เขียน integration tests สำหรับ Member API (CRUD operations):

- POST /api/members - สร้าง member ใหม่
- GET /api/members - ดึงข้อมูล members ทั้งหมด
- GET /api/members/:id - ดึงข้อมูล member ตามรหัส
- PUT /api/members/:id - อัปเดต member
- DELETE /api/members/:id - ลบ member

### Challenge 2: Borrowing Management Integration Tests

เขียน integration tests สำหรับ Borrowing API:

- POST /api/borrowing - ยืมหนังสือ
- GET /api/borrowing - ดึงข้อมูลการยืมทั้งหมด
- GET /api/borrowing/:id - ดึงข้อมูลการยืมตามรหัส
- PUT /api/borrowing/:id - อัปเดตการยืม
- POST /api/borrowing/:id/return - คืนหนังสือ

### Challenge 3: Search Functionality Tests

ทดสอบ advanced search features:

- Filter by author (/api/books/search?author=xxx)
- Filter by year range (/api/books/search?year=xxx)
- Search members (/api/members/search?q=xxx)
- Pagination

### Challenge 4: Concurrent Borrowing Tests

ทดสอบกรณีที่หลายคนพยายามยืมหนังสือเล่มสุดท้ายพร้อมกัน

### Challenge 5: Performance Testing

เพิ่ม performance assertions:

```javascript
test("API response time should be under 200ms", async () => {
  const start = Date.now();
  await request(app).get("/api/books");
  const duration = Date.now() - start;

  expect(duration).toBeLessThan(200);
});
```

---

## 📚 Additional Resources

- [Supertest Documentation](https://github.com/visionmedia/supertest)
- [Ajv JSON Schema Validator](https://ajv.js.org/)
- [JSON Schema Tutorial](https://json-schema.org/learn/)
- [Jest Testing Guide](https://jestjs.io/docs/getting-started)

---

**Next Week:** System Testing และ Acceptance Testing 🎯
