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
39 changes: 39 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
name: CI

on:
push:
branches:
- main
pull_request:

jobs:
ci:
runs-on: ubuntu-latest

steps:
- name: Checkout repository
uses: actions/checkout@v4

- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'npm'

- name: Install dependencies
run: npm ci

- name: Check formatting
run: npx biome format .

- name: Lint
run: npx biome lint .

- name: Type check
run: npm run typecheck

- name: Build
run: npm run build

- name: Run tests
run: npm test
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,12 @@
"scripts": {
"build": "tsc && chmod +x dist/index.js",
"dev": "tsx src/index.ts",
"typecheck": "tsc --noEmit",
"lint": "biome lint --write .",
"format": "biome format --write .",
"inspector": "npx @modelcontextprotocol/inspector tsx src/index.ts",
"test": "vitest run",
"test:unit": "vitest run src/tests/unit",
"prepare": "husky"
},
"dependencies": {
Expand Down
8 changes: 8 additions & 0 deletions src/schemas.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,11 @@ export const projectCodeSchema = z
.string()
.regex(/^[A-Z0-9]{2,5}$/, 'Marker must be 2 to 5 characters in format PROJECT_CODE (e.g., BDI)')
.describe('Project code identifier (e.g., BDI)')

export const testCaseMarkerSchema = z
.string()
.regex(
/^[A-Z0-9]{2,5}-\d+$/,
'Marker must be in format PROJECT_CODE-SEQUENCE (e.g., BDI-123). Project code must be 2 to 5 characters in format PROJECT_CODE (e.g., BDI). Sequence must be a number.'
)
.describe('Test case marker in format PROJECT_CODE-SEQUENCE (e.g., BDI-123)')
27 changes: 27 additions & 0 deletions src/tests/fixtures/customFields.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
export const mockCustomField = {
id: 1,
systemName: 'test_environment',
title: 'Test Environment',
type: 'dropdown',
isEnabled: true,
options: [
{ id: 1, value: 'dev', label: 'Development' },
{ id: 2, value: 'staging', label: 'Staging' },
{ id: 3, value: 'prod', label: 'Production' },
],
defaultValue: 'dev',
}

export const mockCustomFields = {
data: [
mockCustomField,
{
id: 2,
systemName: 'test_type',
title: 'Test Type',
type: 'text',
isEnabled: true,
defaultValue: '',
},
],
}
29 changes: 29 additions & 0 deletions src/tests/fixtures/folders.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
export const mockFolder = {
id: 1,
title: 'Test Folder',
projectId: 'uuid-123',
pos: 0,
parentId: null,
createdAt: '2024-01-01T00:00:00Z',
updatedAt: '2024-01-01T00:00:00Z',
}

export const mockFolders = {
data: [
mockFolder,
{
id: 2,
title: 'Nested Folder',
projectId: 'uuid-123',
pos: 0,
parentId: 1,
createdAt: '2024-01-02T00:00:00Z',
updatedAt: '2024-01-02T00:00:00Z',
},
],
total: 2,
page: 1,
limit: 100,
}

export const mockUpsertFoldersResponse = [[1], [1, 2]]
24 changes: 24 additions & 0 deletions src/tests/fixtures/projects.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
export const mockProject = {
id: 'uuid-123',
title: 'Test Project',
code: 'TST',
createdAt: '2024-01-01T00:00:00Z',
updatedAt: '2024-01-01T00:00:00Z',
}

export const mockProjects = {
projects: [
mockProject,
{
id: 'uuid-456',
title: 'Another Project',
code: 'BDI',
createdAt: '2024-01-02T00:00:00Z',
updatedAt: '2024-01-02T00:00:00Z',
},
],
}

export const mockProjectsEmpty = {
projects: [],
}
31 changes: 31 additions & 0 deletions src/tests/fixtures/requirements.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
export const mockRequirement = {
id: 'req-1',
text: 'User Authentication',
url: 'https://jira.example.com/PROJ-123',
}

export const mockRequirements = {
requirements: [
mockRequirement,
{
id: 'req-2',
text: 'User Authorization',
url: 'https://jira.example.com/PROJ-124',
},
],
}

export const mockRequirementsWithCount = {
requirements: [
{
...mockRequirement,
tcaseCount: 5,
},
{
id: 'req-2',
text: 'User Authorization',
url: 'https://jira.example.com/PROJ-124',
tcaseCount: 3,
},
],
}
23 changes: 23 additions & 0 deletions src/tests/fixtures/sharedPreconditions.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
export const mockSharedPrecondition = {
id: 1,
title: 'User is logged in',
text: '<p>User must be authenticated before running this test</p>',
createdAt: '2024-01-01T00:00:00Z',
}

export const mockSharedPreconditions = [
mockSharedPrecondition,
{
id: 2,
title: 'Database is populated',
text: '<p>Test database must have sample data</p>',
createdAt: '2024-01-02T00:00:00Z',
},
]

export const mockSharedPreconditionsWithCount = [
{
...mockSharedPrecondition,
tcaseCount: 15,
},
]
41 changes: 41 additions & 0 deletions src/tests/fixtures/sharedSteps.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
export const mockSharedStep = {
id: 1,
title: 'Login Step',
subSteps: [
{
description: 'Enter username',
expected: 'Username field populated',
},
{
description: 'Enter password',
expected: 'Password field populated',
},
],
createdAt: '2024-01-01T00:00:00Z',
}

export const mockSharedSteps = {
sharedSteps: [
mockSharedStep,
{
id: 2,
title: 'Logout Step',
subSteps: [
{
description: 'Click logout button',
expected: 'User logged out',
},
],
createdAt: '2024-01-02T00:00:00Z',
},
],
}

export const mockSharedStepsWithCount = {
sharedSteps: [
{
...mockSharedStep,
tcaseCount: 10,
},
],
}
22 changes: 22 additions & 0 deletions src/tests/fixtures/tags.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
export const mockTag = {
id: 1,
title: 'smoke',
projectId: 'uuid-123',
createdAt: '2024-01-01T00:00:00Z',
}

export const mockTags = {
data: [
mockTag,
{
id: 2,
title: 'regression',
projectId: 'uuid-123',
createdAt: '2024-01-02T00:00:00Z',
},
],
}

export const mockTagsEmpty = {
data: [],
}
41 changes: 41 additions & 0 deletions src/tests/fixtures/testcases.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
export const mockTestCase = {
id: 'uuid-456',
title: 'Login Test',
version: 1,
priority: 'high',
steps: [
{ description: 'Step 1', expected: 'Result 1' },
{ description: 'Step 2', expected: 'Result 2' },
],
}

export const mockTestCasesList = {
data: [
mockTestCase,
{
id: 'uuid-789',
title: 'Logout Test',
version: 1,
priority: 'medium',
steps: [],
},
],
total: 2,
page: 1,
limit: 20,
}

export const mockTestCasesEmpty = {
data: [],
total: 0,
page: 1,
limit: 20,
}

export const mockCreateTestCaseResponse = {
tcase: {
id: 'uuid-new',
title: 'New Test Case',
seq: 123,
},
}
8 changes: 8 additions & 0 deletions src/tests/setup.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import type { Mock } from 'vitest'

export type MockedAxios = {
get: Mock
post: Mock
patch: Mock
isAxiosError: Mock
}
66 changes: 66 additions & 0 deletions src/tests/unit/config.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'

describe('Config Module Tests', () => {
let originalEnv: NodeJS.ProcessEnv

beforeEach(() => {
originalEnv = { ...process.env }
})

afterEach(() => {
process.env = originalEnv
vi.resetModules()
})

describe('URL normalization', () => {
it('should add https:// prefix when protocol is missing', async () => {
process.env.QASPHERE_TENANT_URL = 'tenant.qasphere.com'
process.env.QASPHERE_API_KEY = 'test-key'

const config = await import('../../config.js')
expect(config.QASPHERE_TENANT_URL).toBe('https://tenant.qasphere.com')
})

it('should keep http:// as-is', async () => {
process.env.QASPHERE_TENANT_URL = 'http://tenant.qasphere.com'
process.env.QASPHERE_API_KEY = 'test-key'

const config = await import('../../config.js')
expect(config.QASPHERE_TENANT_URL).toBe('http://tenant.qasphere.com')
})

it('should keep https:// as-is', async () => {
process.env.QASPHERE_TENANT_URL = 'https://tenant.qasphere.com'
process.env.QASPHERE_API_KEY = 'test-key'

const config = await import('../../config.js')
expect(config.QASPHERE_TENANT_URL).toBe('https://tenant.qasphere.com')
})

it('should remove trailing slash', async () => {
process.env.QASPHERE_TENANT_URL = 'https://tenant.qasphere.com/'
process.env.QASPHERE_API_KEY = 'test-key'

const config = await import('../../config.js')
expect(config.QASPHERE_TENANT_URL).toBe('https://tenant.qasphere.com')
})

it('should normalize mixed case protocol', async () => {
process.env.QASPHERE_TENANT_URL = 'HTTPS://tenant.qasphere.com'
process.env.QASPHERE_API_KEY = 'test-key'

const config = await import('../../config.js')
expect(config.QASPHERE_TENANT_URL).toBe('HTTPS://tenant.qasphere.com')
})
})

describe('API key', () => {
it('should export the API key from environment', async () => {
process.env.QASPHERE_TENANT_URL = 'https://tenant.qasphere.com'
process.env.QASPHERE_API_KEY = 'my-secret-key'

const config = await import('../../config.js')
expect(config.QASPHERE_API_KEY).toBe('my-secret-key')
})
})
})
Loading