shortcutClient.test.ts•11.8 kB
import { describe, it, expect, beforeAll } from 'vitest'
import { ShortcutClient } from '../../src/shortcutClient'
// Types are used implicitly for type checking in tests
// @ts-ignore
import type {
Story,
StoryComment,
Workflow,
Project,
Epic,
Member,
SearchResults,
} from '../../src/shortcut-types'
describe('ShortcutClient Integration Tests', () => {
let client: ShortcutClient
let testStoryId: number | null = null
let testProjectId: number | null = null
let testEpicId: number | null = null
let testWorkflowStateId: number | null = null
beforeAll(() => {
const apiToken = process.env.SHORTCUT_TOKEN
if (!apiToken) {
throw new Error('SHORTCUT_TOKEN not found in environment')
}
client = new ShortcutClient({
apiToken,
baseUrl: 'https://api.app.shortcut.com/api/v3',
})
})
describe('getWorkflows', () => {
it('should fetch workflows with states', async () => {
const workflows = await client.getWorkflows()
expect(workflows).toBeDefined()
expect(Array.isArray(workflows)).toBe(true)
expect(workflows.length).toBeGreaterThan(0)
const workflow = workflows[0]!
expect(workflow).toHaveProperty('id')
expect(workflow).toHaveProperty('name')
expect(workflow).toHaveProperty('states')
expect(Array.isArray(workflow.states)).toBe(true)
expect(workflow.states.length).toBeGreaterThan(0)
const state = workflow.states[0]!
expect(state).toHaveProperty('id')
expect(state).toHaveProperty('name')
expect(state).toHaveProperty('type')
expect(['unstarted', 'started', 'done']).toContain(state.type)
expect(state).toHaveProperty('position')
// Save a workflow state ID for later tests
testWorkflowStateId = workflow.states[0]!.id
})
})
describe('getProjects', () => {
it('should fetch all projects', async () => {
const projects = await client.getProjects()
expect(projects).toBeDefined()
expect(Array.isArray(projects)).toBe(true)
if (projects.length > 0) {
const project = projects[0]!
expect(project).toHaveProperty('id')
expect(project).toHaveProperty('name')
expect(project).toHaveProperty('color')
expect(project).toHaveProperty('team_id')
// Save a project ID for later tests
testProjectId = project.id
}
})
})
describe('getEpics', () => {
it('should fetch all epics', async () => {
const epics = await client.getEpics()
expect(epics).toBeDefined()
expect(Array.isArray(epics)).toBe(true)
if (epics.length > 0) {
const epic = epics[0]!
expect(epic).toHaveProperty('id')
expect(epic).toHaveProperty('name')
expect(epic).toHaveProperty('state')
expect(['to do', 'in progress', 'done']).toContain(epic.state)
// Save an epic ID for later tests
testEpicId = epic.id
}
})
})
describe('getMembers', () => {
it('should fetch all members', async () => {
const members = await client.getMembers()
expect(members).toBeDefined()
expect(Array.isArray(members)).toBe(true)
expect(members.length).toBeGreaterThan(0)
const member = members[0]!
expect(member).toHaveProperty('id')
// Members might have different structures depending on role/type
// Let's just verify we got a valid member object with an ID
expect(typeof member.id).toBe('string')
})
})
describe('getCurrentUser', () => {
it('should fetch the current user', async () => {
const user = await client.getCurrentUser()
expect(user).toBeDefined()
expect(user).toHaveProperty('id')
// The current user endpoint might return a more complete member object
expect(typeof user.id).toBe('string')
})
})
describe('searchStories', () => {
it('should search for stories', async () => {
const results = await client.searchStories('is:story')
expect(results).toBeDefined()
expect(results).toHaveProperty('stories')
expect(results.stories).toHaveProperty('data')
expect(results.stories).toHaveProperty('total')
expect(Array.isArray(results.stories.data)).toBe(true)
if (results.stories.data.length > 0) {
const story = results.stories.data[0]!
expect(story).toHaveProperty('id')
expect(story).toHaveProperty('name')
expect(story).toHaveProperty('story_type')
expect(['feature', 'bug', 'chore']).toContain(story.story_type)
// Save a story ID for later tests
testStoryId = story.id
}
})
it('should handle search with specific query', async () => {
const results = await client.searchStories('state:unstarted')
expect(results).toBeDefined()
expect(results.stories).toBeDefined()
expect(Array.isArray(results.stories.data)).toBe(true)
})
})
describe('getStoriesInProject', () => {
it('should fetch stories in a project', async () => {
if (!testProjectId) {
console.warn('No test project ID available, skipping test')
return
}
const stories = await client.getStoriesInProject(testProjectId)
expect(stories).toBeDefined()
expect(Array.isArray(stories)).toBe(true)
if (stories.length > 0) {
const story = stories[0]!
expect(story.project_id).toBe(testProjectId)
}
})
})
describe('getStoriesInEpic', () => {
it('should fetch stories in an epic', async () => {
if (!testEpicId) {
console.warn('No test epic ID available, skipping test')
return
}
const stories = await client.getStoriesInEpic(testEpicId)
expect(stories).toBeDefined()
expect(Array.isArray(stories)).toBe(true)
if (stories.length > 0) {
const story = stories[0]!
expect(story.epic_id).toBe(testEpicId)
}
})
})
describe('getStory', () => {
it('should fetch a specific story by ID', async () => {
if (!testStoryId) {
console.warn('No test story ID available, skipping test')
return
}
const story = await client.getStory(testStoryId)
expect(story).toBeDefined()
expect(story.id).toBe(testStoryId)
expect(story).toHaveProperty('name')
expect(story).toHaveProperty('description')
expect(story).toHaveProperty('story_type')
expect(story).toHaveProperty('workflow_state_id')
expect(story).toHaveProperty('created_at')
expect(story).toHaveProperty('updated_at')
})
it('should handle non-existent story ID', async () => {
await expect(client.getStory(999999999)).rejects.toThrow()
})
})
describe('getComments', () => {
it('should fetch comments for a story', async () => {
if (!testStoryId) {
console.warn('No test story ID available, skipping test')
return
}
const comments = await client.getComments(testStoryId)
expect(comments).toBeDefined()
expect(Array.isArray(comments)).toBe(true)
if (comments.length > 0) {
const comment = comments[0]!
expect(comment).toHaveProperty('id')
expect(comment).toHaveProperty('text')
expect(comment).toHaveProperty('author_id')
expect(comment).toHaveProperty('created_at')
expect(comment).toHaveProperty('story_id')
expect(comment.story_id).toBe(testStoryId)
}
})
})
describe('Story modifications', () => {
it.skip('should add a comment to a story', async () => {
if (!testStoryId) {
console.warn('No test story ID available, skipping test')
return
}
const commentText = `Integration test comment - ${new Date().toISOString()}`
const comment = await client.addComment(testStoryId, {
text: commentText,
})
expect(comment).toBeDefined()
expect(comment).toHaveProperty('id')
expect(comment.text).toBe(commentText)
expect(comment.story_id).toBe(testStoryId)
})
it.skip('should update a story', async () => {
if (!testStoryId || !testWorkflowStateId) {
console.warn('No test story ID or workflow state ID available, skipping test')
return
}
const originalStory = await client.getStory(testStoryId)
const updateDescription = `Updated by integration test - ${new Date().toISOString()}`
const updatedStory = await client.updateStory(testStoryId, {
description: updateDescription,
})
expect(updatedStory).toBeDefined()
expect(updatedStory.id).toBe(testStoryId)
expect(updatedStory.description).toBe(updateDescription)
// Restore original description
if (originalStory.description !== undefined) {
await client.updateStory(testStoryId, {
description: originalStory.description,
})
}
})
it.skip('should update story with labels', async () => {
if (!testStoryId) {
console.warn('No test story ID available, skipping test')
return
}
const updatedStory = await client.updateStory(testStoryId, {
labels: [{ name: 'test-label' }],
})
expect(updatedStory).toBeDefined()
expect(updatedStory.labels).toBeDefined()
expect(Array.isArray(updatedStory.labels)).toBe(true)
})
})
describe('Error handling', () => {
it('should handle API errors gracefully', async () => {
const invalidClient = new ShortcutClient({
apiToken: 'invalid-token',
baseUrl: 'https://api.app.shortcut.com/api/v3',
})
await expect(invalidClient.getProjects()).rejects.toThrow('Shortcut API error')
})
it('should include error details in the error object', async () => {
const invalidClient = new ShortcutClient({
apiToken: 'invalid-token',
baseUrl: 'https://api.app.shortcut.com/api/v3',
})
try {
await invalidClient.getProjects()
expect.fail('Should have thrown an error')
} catch (error: any) {
expect(error).toBeInstanceOf(Error)
expect(error.message).toContain('Shortcut API error')
expect(error.response).toBeDefined()
expect(error.response.status).toBeDefined()
}
})
})
})