response-parsing.test.ts•8.23 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('Shortcut API Response Parsing', () => {
let client: ShortcutClient
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('Type validation', () => {
it('should parse workflow response correctly', async () => {
const workflows = await client.getWorkflows()
workflows.forEach((workflow) => {
expect(workflow).toMatchObject({
id: expect.any(Number),
name: expect.any(String),
states: expect.any(Array),
})
workflow.states.forEach((state) => {
expect(state).toMatchObject({
id: expect.any(Number),
name: expect.any(String),
type: expect.stringMatching(/^(unstarted|started|done)$/),
position: expect.any(Number),
})
})
})
})
it('should parse project response correctly', async () => {
const projects = await client.getProjects()
projects.forEach((project) => {
expect(project).toMatchObject({
id: expect.any(Number),
name: expect.any(String),
color: expect.stringMatching(/^#[0-9a-fA-F]{6}$/),
team_id: expect.any(Number),
})
if (project.description !== undefined) {
expect(typeof project.description).toBe('string')
}
})
})
it('should parse epic response correctly', async () => {
const epics = await client.getEpics()
epics.forEach((epic) => {
expect(epic).toMatchObject({
id: expect.any(Number),
name: expect.any(String),
state: expect.stringMatching(/^(to do|in progress|done)$/),
})
if (epic.description !== undefined) {
expect(typeof epic.description).toBe('string')
}
if (epic.milestone_id !== undefined && epic.milestone_id !== null) {
expect(typeof epic.milestone_id).toBe('number')
}
})
})
it('should parse story response correctly', async () => {
const results = await client.searchStories('is:story')
if (results.stories.data.length > 0) {
const story = results.stories.data[0]!
expect(story).toMatchObject({
id: expect.any(Number),
name: expect.any(String),
story_type: expect.stringMatching(/^(feature|bug|chore)$/),
workflow_state_id: expect.any(Number),
created_at: expect.any(String),
updated_at: expect.any(String),
started: expect.any(Boolean),
completed: expect.any(Boolean),
})
// Optional fields
if (story.description !== undefined) {
expect(typeof story.description).toBe('string')
}
if (story.estimate !== undefined && story.estimate !== null) {
expect(typeof story.estimate).toBe('number')
}
if (story.project_id !== undefined && story.project_id !== null) {
expect(typeof story.project_id).toBe('number')
}
if (story.epic_id !== undefined && story.epic_id !== null) {
expect(typeof story.epic_id).toBe('number')
}
if (story.labels !== undefined) {
expect(Array.isArray(story.labels)).toBe(true)
story.labels.forEach((label) => {
expect(label).toMatchObject({
id: expect.any(Number),
name: expect.any(String),
})
})
}
if (story.owner_ids !== undefined) {
expect(Array.isArray(story.owner_ids)).toBe(true)
story.owner_ids.forEach((id: string) => {
expect(typeof id).toBe('string')
})
}
}
})
it('should parse search results correctly', async () => {
const results = await client.searchStories('is:story')
expect(results).toMatchObject({
stories: {
data: expect.any(Array),
total: expect.any(Number),
},
epics: {
data: expect.any(Array),
total: expect.any(Number),
},
})
expect(results.stories.total).toBeGreaterThanOrEqual(results.stories.data.length)
expect(results.epics.total).toBeGreaterThanOrEqual(results.epics.data.length)
})
it('should parse comment response correctly', async () => {
// First, find a story with comments
const stories = await client.searchStories('is:story has:comment')
if (stories.stories.data.length > 0) {
const storyId = stories.stories.data[0]!.id
const comments = await client.getComments(storyId)
if (comments.length > 0) {
comments.forEach((comment) => {
expect(comment).toMatchObject({
id: expect.any(Number),
text: expect.any(String),
author_id: expect.any(String),
created_at: expect.any(String),
updated_at: expect.any(String),
story_id: expect.any(Number),
})
})
}
}
})
it('should handle empty arrays correctly', async () => {
// Search for something unlikely to return results
const results = await client.searchStories('is:story label:"nonexistent-label-xyz-123"')
expect(results.stories.data).toEqual([])
expect(results.stories.total).toBe(0)
})
it('should handle special characters in responses', async () => {
// Test searching with special characters
const results = await client.searchStories('is:story "test & development"')
// Should not throw and should return valid structure
expect(results).toHaveProperty('stories')
expect(results.stories).toHaveProperty('data')
expect(results.stories).toHaveProperty('total')
})
})
describe('Error response parsing', () => {
it('should parse error responses with proper structure', async () => {
try {
await client.getStory(999999999)
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()
expect(typeof error.response.status).toBe('number')
if (error.response.data) {
expect(typeof error.response.data).toBe('object')
}
}
})
})
})