Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
# QA Sphere Integration Test Configuration
# Copy this file to .env and fill in the required values

# Required: API key for public API access
QASPHERE_API_KEY=

# Required: Authentication credentials
QASPHERE_AUTH_EMAIL=
QASPHERE_AUTH_PASSWORD=

# Optional: Override defaults for different environments
# QASPHERE_TENANT_URL=https://e2eqas.eu1.qasphere.com
# QASPHERE_TENANT_ID=eu1vweq68d
12 changes: 10 additions & 2 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,11 @@ on:
jobs:
ci:
runs-on: ubuntu-latest
env:
QASPHERE_TENANT_URL: https://e2eqas.eu1.qasphere.com
QASPHERE_API_KEY: ${{ secrets.QASPHERE_API_KEY }}
QASPHERE_AUTH_EMAIL: ${{ secrets.QASPHERE_AUTH_EMAIL }}
QASPHERE_AUTH_PASSWORD: ${{ secrets.QASPHERE_AUTH_PASSWORD }}
Comment on lines +15 to +16
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why do we need email and password? Using an API key seems enough. We never use username and password for public APIs.


steps:
- name: Checkout repository
Expand All @@ -35,5 +40,8 @@ jobs:
- name: Build
run: npm run build

- name: Run tests
run: npm test
- name: Run unit tests
run: npm run test:unit

- name: Run integration tests
run: npm run test:integration
3 changes: 2 additions & 1 deletion .husky/pre-commit
100644 → 100755
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
npx lint-staged
npx lint-staged
npm run typecheck
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,8 @@
"format": "biome format --write .",
"inspector": "npx @modelcontextprotocol/inspector tsx src/index.ts",
"test": "vitest run",
"test:unit": "vitest run src/tests/unit",
"test:unit": "vitest run",
"test:integration": "vitest run -c vitest.integration.config.ts",
"prepare": "husky"
},
"dependencies": {
Expand Down
72 changes: 72 additions & 0 deletions src/tests/integration/customFields.integration.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
import { describe, it, expect, beforeAll, afterAll } from 'vitest'
import axios from 'axios'
import {
getTenantUrl,
getApiHeaders,
login,
createTestProject,
deleteTestProject,
generateProjectCode,
} from './helpers.js'

describe('Custom Fields API Integration Tests', () => {
let sessionToken: string
let testProjectCode: string
let testProjectId: string

beforeAll(async () => {
// Login and create test project
sessionToken = await login()
testProjectCode = generateProjectCode()
const project = await createTestProject(
sessionToken,
testProjectCode,
`[MCP-TEST] ${testProjectCode}`
)
testProjectId = project.id
})

afterAll(async () => {
// Clean up the test project (only if it was created)
if (sessionToken && testProjectId) {
await deleteTestProject(sessionToken, testProjectId)
}
})

describe('list_custom_fields', () => {
it('should return field definitions', async () => {
const response = await axios.get(
`${getTenantUrl()}/api/public/v0/project/${testProjectCode}/custom-field`,
{
headers: getApiHeaders(),
}
)

expect(response.status).toBe(200)
expect(response.data).toHaveProperty('customFields')
expect(Array.isArray(response.data.customFields)).toBe(true)
})

it('should return fields with correct types', async () => {
const response = await axios.get(
`${getTenantUrl()}/api/public/v0/project/${testProjectCode}/custom-field`,
{
headers: getApiHeaders(),
}
)

expect(response.status).toBe(200)

// If there are custom fields, verify they have the expected structure
const customFields = response.data.customFields
if (customFields.length > 0) {
const field = customFields[0]
expect(field).toHaveProperty('id')
expect(field).toHaveProperty('systemName')
expect(field).toHaveProperty('name')
expect(field).toHaveProperty('type')
expect(['text', 'dropdown']).toContain(field.type)
}
})
})
})
162 changes: 162 additions & 0 deletions src/tests/integration/folders.integration.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,162 @@
import { describe, it, expect, beforeAll, afterAll } from 'vitest'
import axios from 'axios'
import {
getTenantUrl,
getApiHeaders,
login,
createTestProject,
deleteTestProject,
generateProjectCode,
} from './helpers.js'

describe('Folder API Integration Tests', () => {
let sessionToken: string
let testProjectCode: string
let testProjectId: string

beforeAll(async () => {
// Login and create test project
sessionToken = await login()
testProjectCode = generateProjectCode()
const project = await createTestProject(
sessionToken,
testProjectCode,
`[MCP-TEST] ${testProjectCode}`
)
testProjectId = project.id
})

afterAll(async () => {
// Clean up the test project (only if it was created)
if (sessionToken && testProjectId) {
await deleteTestProject(sessionToken, testProjectId)
}
})

describe('list_folders', () => {
it('should return folder list with pagination', async () => {
const response = await axios.get(
`${getTenantUrl()}/api/public/v0/project/${testProjectCode}/tcase/folders`,
{
headers: getApiHeaders(),
}
)

expect(response.status).toBe(200)
expect(response.data).toHaveProperty('data')
expect(response.data).toHaveProperty('total')
expect(response.data).toHaveProperty('page')
expect(response.data).toHaveProperty('limit')
expect(Array.isArray(response.data.data)).toBe(true)
})

it('should respect pagination parameters', async () => {
// First create some folders
await axios.post(
`${getTenantUrl()}/api/public/v0/project/${testProjectCode}/tcase/folder/bulk`,
{
folders: [
{ path: ['[MCP-TEST] Folder 1'] },
{ path: ['[MCP-TEST] Folder 2'] },
{ path: ['[MCP-TEST] Folder 3'] },
{ path: ['[MCP-TEST] Folder 4'] },
{ path: ['[MCP-TEST] Folder 5'] },
{ path: ['[MCP-TEST] Folder 6'] },
],
},
{
headers: getApiHeaders(),
}
)

const response = await axios.get(
`${getTenantUrl()}/api/public/v0/project/${testProjectCode}/tcase/folders`,
{
params: { page: 1, limit: 5 },
headers: getApiHeaders(),
}
)

expect(response.status).toBe(200)
expect(response.data.page).toBe(1)
expect(response.data.limit).toBe(5)
expect(response.data.data.length).toBeLessThanOrEqual(5)
})
})

describe('upsert_folders', () => {
it('should create single folder', async () => {
const response = await axios.post(
`${getTenantUrl()}/api/public/v0/project/${testProjectCode}/tcase/folder/bulk`,
{
folders: [{ path: ['[MCP-TEST] Single Folder'] }],
},
{
headers: getApiHeaders(),
}
)

expect(response.status).toBe(200)
expect(response.data).toHaveProperty('ids')
expect(Array.isArray(response.data.ids)).toBe(true)
expect(response.data.ids.length).toBe(1)
expect(response.data.ids[0].length).toBe(1)
expect(typeof response.data.ids[0][0]).toBe('number')
})

it('should create nested folders', async () => {
const response = await axios.post(
`${getTenantUrl()}/api/public/v0/project/${testProjectCode}/tcase/folder/bulk`,
{
folders: [{ path: ['[MCP-TEST] A', '[MCP-TEST] B', '[MCP-TEST] C'] }],
},
{
headers: getApiHeaders(),
}
)

expect(response.status).toBe(200)
expect(response.data).toHaveProperty('ids')
expect(response.data.ids.length).toBe(1)
expect(response.data.ids[0].length).toBe(3)

// Verify each level has a valid ID
for (const id of response.data.ids[0]) {
expect(typeof id).toBe('number')
expect(id).toBeGreaterThan(0)
}
})

it('should update folder comment without creating duplicate', async () => {
const folderPath = ['[MCP-TEST] Update Test']

// Create folder
const createResponse = await axios.post(
`${getTenantUrl()}/api/public/v0/project/${testProjectCode}/tcase/folder/bulk`,
{
folders: [{ path: folderPath, comment: 'Initial comment' }],
},
{
headers: getApiHeaders(),
}
)

const folderId = createResponse.data.ids[0][0]

// Update folder comment
const updateResponse = await axios.post(
`${getTenantUrl()}/api/public/v0/project/${testProjectCode}/tcase/folder/bulk`,
{
folders: [{ path: folderPath, comment: 'Updated comment' }],
},
{
headers: getApiHeaders(),
}
)

expect(updateResponse.status).toBe(200)
// Should return the same folder ID (not create a new one)
expect(updateResponse.data.ids[0][0]).toBe(folderId)
})
})
})
Loading