ai

Directus CMS Testing Strategy QA

Directus CMS Testing Strategy QA

ทำไมต้องทำ Testing Strategy สำหรับ Directus CMS

Directus CMS Testing Strategy QA

Directus เป็น headless CMS แบบ open source ที่สร้าง REST และ GraphQL API จาก database โดยตรง เมื่อใช้ Directus เป็น backend ให้กับ production app ต้องมี testing strategy ที่ครอบคลุมทั้ง API layer, custom extensions, data validation และ integration กับ frontend เพราะ Directus ให้อิสระในการออกแบบ schema มาก ถ้าไม่มี test รองรับจะเจอปัญหา regression ทุกครั้งที่ upgrade version หรือเปลี่ยน schema

ติดตั้ง Directus สำหรับ Testing Environment

# docker-compose.yml สำหรับ test environment

version: '3.8'

services:

  directus:

    image: directus/directus:10.10.0

    ports:

      - "8055:8055"

    environment:

      KEY: "test-key-not-for-production"

      SECRET: "test-secret-not-for-production"

      ADMIN_EMAIL: "admin@test.local"

      ADMIN_PASSWORD: "TestPassword123!"

      DB_CLIENT: "pg"

      DB_HOST: "postgres"

      DB_PORT: "5432"

      DB_DATABASE: "directus_test"

      DB_USER: "directus"

      DB_PASSWORD: "directus_test_pass"

      CACHE_ENABLED: "false"

      RATE_LIMITER_ENABLED: "false"

    depends_on:

      postgres:

        condition: service_healthy



  postgres:

    image: postgres:16-alpine

    environment:

      POSTGRES_DB: directus_test

      POSTGRES_USER: directus

      POSTGRES_PASSWORD: directus_test_pass

    healthcheck:

      test: ["CMD-SHELL", "pg_isready -U directus"]

      interval: 5s

      timeout: 3s

      retries: 5

    volumes:

      - pgdata:/var/lib/postgresql/data



volumes:

  pgdata:
# รัน test environment

docker compose up -d



# รอจน Directus พร้อม

until curl -s http://localhost:8055/server/health | grep -q '"status":"ok"'; do

  echo "Waiting for Directus..."

  sleep 2

done

echo "Directus is ready"

API Testing ด้วย Jest และ Axios

ทดสอบ Directus API endpoints ทั้ง CRUD operations, filtering, permissions และ custom endpoints

# ติดตั้ง dependencies สำหรับ test

npm init -y

npm install --save-dev jest @types/jest ts-jest axios dotenv

npm install --save-dev @directus/sdk
// jest.config.js

module.exports = {

  preset: 'ts-jest',

  testEnvironment: 'node',

  testTimeout: 30000,

  setupFilesAfterSetup: ['./tests/setup.ts'],

  testMatch: ['**/tests/**/*.test.ts'],

};
// tests/setup.ts

import axios from 'axios';



const DIRECTUS_URL = process.env.DIRECTUS_URL || 'http://localhost:8055';



export async function getAdminToken(): Promise<string> {

  const resp = await axios.post(`/auth/login`, {

    email: 'admin@test.local',

    password: 'TestPassword123!',

  });

  return resp.data.data.access_token;

}



export async function setupTestCollection(token: string) {

  const api = axios.create({

    baseURL: DIRECTUS_URL,

    headers: { Authorization: `Bearer ` },

  });



  // สร้าง collection สำหรับ test

  try {

    await api.post('/collections', {

      collection: 'test_articles',

      meta: { icon: 'article', note: 'Test collection' },

      schema: {},

      fields: [

        { field: 'id', type: 'integer', meta: { hidden: true, interface: 'input' }, schema: { is_primary_key: true, has_auto_increment: true } },

        { field: 'title', type: 'string', meta: { interface: 'input' }, schema: { is_nullable: false } },

        { field: 'content', type: 'text', meta: { interface: 'input-rich-text-md' } },

        { field: 'status', type: 'string', meta: { interface: 'select-dropdown', options: { choices: [{ text: 'Draft', value: 'draft' }, { text: 'Published', value: 'published' }] } }, schema: { default_value: 'draft' } },

        { field: 'publish_date', type: 'timestamp', meta: { interface: 'datetime' } },

      ],

    });

  } catch (e: any) {

    if (e.response?.status !== 400) throw e; // 400 = already exists

  }

}
// tests/crud.test.ts

import axios, { AxiosInstance } from 'axios';

import { getAdminToken, setupTestCollection } from './setup';



const DIRECTUS_URL = process.env.DIRECTUS_URL || 'http://localhost:8055';

let api: AxiosInstance;

let createdId: number;



beforeAll(async () => {

  const token = await getAdminToken();

  api = axios.create({

    baseURL: DIRECTUS_URL,

    headers: { Authorization: `Bearer ` },

  });

  await setupTestCollection(token);

});



describe('Directus CRUD Operations', () => {

  test('CREATE - สร้าง article ใหม่', async () => {

    const resp = await api.post('/items/test_articles', {

      title: 'ทดสอบบทความ',

      content: 'เนื้อหาทดสอบ',

      status: 'draft',

    });

    expect(resp.status).toBe(200);

    expect(resp.data.data.title).toBe('ทดสอบบทความ');

    createdId = resp.data.data.id;

  });



  test('READ - อ่าน article ที่สร้าง', async () => {

    const resp = await api.get(`/items/test_articles/`);

    expect(resp.status).toBe(200);

    expect(resp.data.data.id).toBe(createdId);

    expect(resp.data.data.status).toBe('draft');

  });



  test('UPDATE - แก้ไข title', async () => {

    const resp = await api.patch(`/items/test_articles/`, {

      title: 'บทความที่แก้ไขแล้ว',

      status: 'published',

    });

    expect(resp.status).toBe(200);

    expect(resp.data.data.title).toBe('บทความที่แก้ไขแล้ว');

  });



  test('FILTER - ค้นหาด้วย filter', async () => {

    const resp = await api.get('/items/test_articles', {

      params: {

        'filter[status][_eq]': 'published',

        'fields': 'id, title, status',

        'sort': '-id',

        'limit': 10,

      },

    });

    expect(resp.status).toBe(200);

    expect(resp.data.data.length).toBeGreaterThan(0);

    expect(resp.data.data[0].status).toBe('published');

  });



  test('DELETE - ลบ article', async () => {

    const resp = await api.delete(`/items/test_articles/`);

    expect(resp.status).toBe(204);



    // ยืนยันว่าลบแล้ว

    try {

      await api.get(`/items/test_articles/`);

      fail('Should have thrown 403/404');

    } catch (e: any) {

      expect([403, 404]).toContain(e.response.status);

    }

  });

});

Permission Testing — ทดสอบระบบ Access Control

Directus CMS Testing Strategy QA

Directus มี granular permission system ที่ต้องทดสอบให้ครบทุก role เพื่อป้องกัน data leak

เนื้อหาเกี่ยวข้อง — แนะนำให้อ่าน LLM Quantization GGUF CDN Configuration

// tests/permissions.test.ts

import axios, { AxiosInstance } from 'axios';

import { getAdminToken } from './setup';



const DIRECTUS_URL = process.env.DIRECTUS_URL || 'http://localhost:8055';



let adminApi: AxiosInstance;

let editorToken: string;

let editorApi: AxiosInstance;



beforeAll(async () => {

  const adminToken = await getAdminToken();

  adminApi = axios.create({

    baseURL: DIRECTUS_URL,

    headers: { Authorization: `Bearer ` },

  });



  // สร้าง editor role

  const roleResp = await adminApi.post('/roles', {

    name: 'Editor',

    icon: 'edit',

    admin_access: false,

    app_access: true,

  });

  const roleId = roleResp.data.data.id;



  // กำหนด permissions - editor อ่านได้ทุก article แต่แก้ได้เฉพาะ draft

  await adminApi.post('/permissions', {

    role: roleId,

    collection: 'test_articles',

    action: 'read',

    fields: ['*'],

  });

  await adminApi.post('/permissions', {

    role: roleId,

    collection: 'test_articles',

    action: 'update',

    fields: ['title', 'content'],

    permissions: { status: { _eq: 'draft' } },

  });



  // สร้าง editor user

  await adminApi.post('/users', {

    email: 'editor@test.local',

    password: 'EditorPass123!',

    role: roleId,

  });



  // login เป็น editor

  const loginResp = await axios.post(`/auth/login`, {

    email: 'editor@test.local',

    password: 'EditorPass123!',

  });

  editorToken = loginResp.data.data.access_token;

  editorApi = axios.create({

    baseURL: DIRECTUS_URL,

    headers: { Authorization: `Bearer ` },

  });

});



describe('Permission Tests', () => {

  test('Editor ไม่สามารถสร้าง article ได้', async () => {

    try {

      await editorApi.post('/items/test_articles', { title: 'Hacked!' });

      fail('Should not allow create');

    } catch (e: any) {

      expect(e.response.status).toBe(403);

    }

  });



  test('Editor ไม่สามารถแก้ published article ได้', async () => {

    // admin สร้าง published article

    const resp = await adminApi.post('/items/test_articles', {

      title: 'Published Article',

      status: 'published',

    });

    const id = resp.data.data.id;



    try {

      await editorApi.patch(`/items/test_articles/`, {

        title: 'Hacked Title',

      });

      fail('Should not allow editing published articles');

    } catch (e: any) {

      expect(e.response.status).toBe(403);

    }

  });



  test('Editor สามารถแก้ draft article ได้', async () => {

    const resp = await adminApi.post('/items/test_articles', {

      title: 'Draft Article',

      status: 'draft',

    });

    const id = resp.data.data.id;



    const updateResp = await editorApi.patch(`/items/test_articles/`, {

      title: 'Updated Draft',

    });

    expect(updateResp.status).toBe(200);

  });

});

Load Testing ด้วย k6

ทดสอบว่า Directus API รับ load ได้เพียงพอก่อน deploy production

// load-test.js (k6 script)

import http from 'k6/http';

import { check, sleep } from 'k6';



export const options = {

  stages: [

    { duration: '30s', target: 20 },   // ramp up

    { duration: '1m', target: 50 },    // hold

    { duration: '30s', target: 100 },  // spike

    { duration: '30s', target: 0 },    // ramp down

  ],

  thresholds: {

    http_req_duration: ['p(95)<500'],  // 95% ต้องเร็วกว่า 500ms

    http_req_failed: ['rate<0.01'],    // error rate < 1%

  },

};



const BASE = 'http://localhost:8055';

let TOKEN;



export function setup() {

  const loginRes = http.post(`/auth/login`, JSON.stringify({

    email: 'admin@test.local',

    password: 'TestPassword123!',

  }), { headers: { 'Content-Type': 'application/json' } });

  return { token: loginRes.json().data.access_token };

}



export default function (data) {

  const headers = {

    Authorization: `Bearer `,

    'Content-Type': 'application/json',

  };



  // อ่าน articles list

  const listRes = http.get(`/items/test_articles?limit=20&sort=-id`, { headers });

  check(listRes, {

    'list status 200': (r) => r.status === 200,

    'list has data': (r) => r.json().data.length > 0,

  });



  // อ่าน single article

  const items = listRes.json().data;

  if (items.length > 0) {

    const id = items[Math.floor(Math.random() * items.length)].id;

    const getRes = http.get(`/items/test_articles/`, { headers });

    check(getRes, { 'get status 200': (r) => r.status === 200 });

  }



  sleep(0.5);

}
# รัน load test

k6 run load-test.js



# ตัวอย่าง output

#      ✓ list status 200

#      ✓ list has data

#      ✓ get status 200

#

#      http_req_duration..........: avg=45ms min=12ms med=38ms max=890ms p(90)=120ms p(95)=230ms

#      http_req_failed............: 0.00%   ✓ 0  ✗ 4521

#      iterations.................: 4521    75.35/s

CI/CD Pipeline สำหรับ Directus Testing

# .github/workflows/test-directus.yml

name: Directus API Tests

on: [push, pull_request]



jobs:

  test:

    runs-on: ubuntu-latest

    services:

      postgres:

        image: postgres:16-alpine

        env:

          POSTGRES_DB: directus_test

          POSTGRES_USER: directus

          POSTGRES_PASSWORD: directus_test_pass

        ports: ['5432:5432']

        options: --health-cmd="pg_isready" --health-interval=5s --health-timeout=3s --health-retries=5



    steps:

      - uses: actions/checkout@v4



      - name: Start Directus

        run: |

          docker run -d --name directus --network host \

            -e KEY=test-key -e SECRET=test-secret \

            -e ADMIN_EMAIL=admin@test.local \

            -e ADMIN_PASSWORD=TestPassword123! \

            -e DB_CLIENT=pg -e DB_HOST=localhost \

            -e DB_PORT=5432 -e DB_DATABASE=directus_test \

            -e DB_USER=directus -e DB_PASSWORD=directus_test_pass \

            directus/directus:10.10.0



      - name: Wait for Directus

        run: |

          for i in $(seq 1 30); do

            curl -s http://localhost:8055/server/health && break

            sleep 2

          done



      - uses: actions/setup-node@v4

        with: { node-version: '20' }



      - run: npm ci

      - run: npm test -- --coverage



      - name: Upload coverage

        uses: actions/upload-artifact@v4

        with:

          name: coverage

          path: coverage/

FAQ — คำถามที่พบบ่อย

Q: ควรใช้ Directus SDK หรือ Axios ในการเขียน test?

แนะนำเพิ่มเติม — อ่านเพิ่มเติมที่ SiamCafeBook

A: ถ้าต้องการทดสอบ API behavior ตรงๆ ให้ใช้ Axios เพราะเห็น request/response ชัดเจน ถ้าต้องการทดสอบ business logic ที่ใช้ SDK อยู่แล้วให้ใช้ SDK เพื่อให้ test ใกล้เคียง production code

เนื้อหาเกี่ยวข้อง — อ่านต่อ: Airflow DAG Design DevSecOps Integration

Q: ทำ snapshot testing กับ Directus schema ได้ไหม?

A: ได้ ใช้คำสั่ง npx directus schema snapshot ./snapshot.yaml เพื่อ export schema แล้วเก็บใน git ตอน CI ให้เปรียบเทียบกับ snapshot ก่อนหน้าเพื่อตรวจจับ schema change ที่ไม่ตั้งใจ

แนะนำเพิ่มเติม — คอร์สเทรด Forex ที่ iCafeForex

เนื้อหาเกี่ยวข้อง — ดูเพิ่มเติมเรื่อง Whisper Speech Message Queue Design

Q: ทดสอบ Directus Flows (automation) อย่างไร?

A: สร้าง test ที่ trigger event แล้วตรวจ side effect เช่น สร้าง item แล้วเช็คว่า Flow ส่ง webhook หรือสร้าง related item ตามที่ตั้งไว้หรือไม่ ใช้ mock server อย่าง msw หรือ nock รับ webhook request ระหว่าง test

Q: ควร test migration ของ Directus อย่างไร?

เนื้อหาเกี่ยวข้อง — บทความที่เกี่ยวข้อง: WordPress Block Theme SSL TLS Certificate

A: ใช้ npx directus schema apply ./snapshot.yaml กับ test database ก่อน apply กับ production ตรวจสอบว่า migration ไม่ทำ data loss โดยสร้าง seed data ก่อน migrate แล้วเช็คว่า data ยังครบอยู่

XM Legend · เทรดเดอร์ & ผู้สอน Forex 13 ปี

ผู้ก่อตั้ง SiamCafe ตั้งแต่ปี 1997 · เทรดเดอร์สาย Forex มากกว่า 13 ปี ได้รับการยกย่องเป็น XM Legend · แบ่งปันความรู้ Forex, ไอที, AI และการเทรด จากประสบการณ์จริงในตลาดจริง