import { beforeEach, describe, expect, it, jest } from '@jest/globals';
import { ProjectManagementService } from '../../../services/ProjectManagementService';
import { GitHubRepositoryFactory } from '../../../infrastructure/github/GitHubRepositoryFactory';
import { SubIssueService } from '../../../services/SubIssueService';
import { MilestoneService } from '../../../services/MilestoneService';
import { SprintPlanningService } from '../../../services/SprintPlanningService';
import { ProjectStatusService } from '../../../services/ProjectStatusService';
import { ProjectTemplateService } from '../../../services/ProjectTemplateService';
import { ProjectLinkingService } from '../../../services/ProjectLinkingService';
import { ResourceStatus, ResourceType } from '../../../domain/resource-types';
import { ValidationError, ResourceNotFoundError, DomainError } from '../../../domain/errors';
/**
* ProjectManagementService Unit Tests
*
* These tests verify setFieldValue and getFieldValue behavior using manual mock injection.
* The service is instantiated directly with mocked dependencies (Approach B from DI refactoring).
*
* Mock Strategy:
* - Create mock factory with graphql method BEFORE service instantiation
* - Create stub services (not used by setFieldValue/getFieldValue)
* - Instantiate service with mocks directly
*
* Note: The service's actual return format is `"Field ${name} updated successfully"`,
* NOT `"Field value updated successfully for field '${name}'"`.
*/
describe('ProjectManagementService', () => {
let service: ProjectManagementService;
let mockGraphql: jest.MockedFunction<any>;
let mockFactory: GitHubRepositoryFactory;
beforeEach(() => {
// Clear all mocks
jest.clearAllMocks();
// Create mock graphql function BEFORE service instantiation
mockGraphql = jest.fn() as jest.MockedFunction<any>;
// Create mock factory with graphql method pre-configured
mockFactory = {
graphql: mockGraphql,
createProjectRepository: jest.fn(),
createMilestoneRepository: jest.fn(),
createIssueRepository: jest.fn(),
createSprintRepository: jest.fn(),
createAutomationRuleRepository: jest.fn()
} as unknown as GitHubRepositoryFactory;
// Create stub services (not used by setFieldValue/getFieldValue tests)
const mockSubIssue = {} as SubIssueService;
const mockMilestone = {} as MilestoneService;
const mockSprint = {} as SprintPlanningService;
const mockProjectStatus = {} as ProjectStatusService;
const mockTemplate = {} as ProjectTemplateService;
const mockLinking = {} as ProjectLinkingService;
// Instantiate service directly with mocked dependencies
service = new ProjectManagementService(
mockFactory,
mockSubIssue,
mockMilestone,
mockSprint,
mockProjectStatus,
mockTemplate,
mockLinking
);
});
it('should be properly initialized', () => {
expect(service).toBeDefined();
});
describe('setFieldValue', () => {
const mockFieldData = {
projectId: 'PROJECT_ID',
itemId: 'ITEM_ID',
fieldId: 'FIELD_ID'
};
beforeEach(() => {
// Clear previous mock calls but keep the implementation
mockGraphql.mockClear();
});
describe('TEXT field type', () => {
it('should set text field value successfully', async () => {
// Mock field query response for TEXT field (first call)
mockGraphql.mockResolvedValueOnce({
node: {
field: {
id: 'FIELD_ID',
name: 'Description',
dataType: 'TEXT'
}
}
});
// Mock update mutation response (second call)
mockGraphql.mockResolvedValueOnce({
updateProjectV2ItemFieldValue: {
projectV2Item: { id: 'ITEM_ID' }
}
});
const result = await service.setFieldValue({
...mockFieldData,
value: 'Updated description'
});
// Service returns "Field ${name} updated successfully" format
expect(result).toEqual({
success: true,
message: "Field Description updated successfully"
});
expect(mockGraphql).toHaveBeenCalledTimes(2);
});
});
describe('NUMBER field type', () => {
it('should set number field value successfully', async () => {
// Mock field query response for NUMBER field (first call)
mockGraphql.mockResolvedValueOnce({
node: {
field: {
id: 'FIELD_ID',
name: 'Story Points',
dataType: 'NUMBER'
}
}
});
// Mock update mutation response (second call)
mockGraphql.mockResolvedValueOnce({
updateProjectV2ItemFieldValue: {
projectV2Item: { id: 'ITEM_ID' }
}
});
const result = await service.setFieldValue({
...mockFieldData,
value: 5
});
expect(result).toEqual({
success: true,
message: "Field Story Points updated successfully"
});
expect(mockGraphql).toHaveBeenCalledTimes(2);
});
});
describe('DATE field type', () => {
it('should set date field value successfully', async () => {
// Mock field query response for DATE field (first call)
mockGraphql.mockResolvedValueOnce({
node: {
field: {
id: 'FIELD_ID',
name: 'Due Date',
dataType: 'DATE'
}
}
});
// Mock update mutation response (second call)
mockGraphql.mockResolvedValueOnce({
updateProjectV2ItemFieldValue: {
projectV2Item: { id: 'ITEM_ID' }
}
});
const result = await service.setFieldValue({
...mockFieldData,
value: '2024-12-31'
});
expect(result).toEqual({
success: true,
message: "Field Due Date updated successfully"
});
expect(mockGraphql).toHaveBeenCalledTimes(2);
});
});
describe('SINGLE_SELECT field type', () => {
it('should set single select field value successfully', async () => {
// Mock field query response for SINGLE_SELECT field (first call)
mockGraphql.mockResolvedValueOnce({
node: {
field: {
id: 'FIELD_ID',
name: 'Status',
dataType: 'SINGLE_SELECT',
options: [
{ id: 'OPTION_1', name: 'To Do' },
{ id: 'OPTION_2', name: 'In Progress' },
{ id: 'OPTION_3', name: 'Done' }
]
}
}
});
// Mock update mutation response (second call)
mockGraphql.mockResolvedValueOnce({
updateProjectV2ItemFieldValue: {
projectV2Item: { id: 'ITEM_ID' }
}
});
const result = await service.setFieldValue({
...mockFieldData,
value: 'In Progress'
});
expect(result).toEqual({
success: true,
message: "Field Status updated successfully"
});
expect(mockGraphql).toHaveBeenCalledTimes(2);
});
it('should use value directly when option not found (service passes through)', async () => {
// Note: The service doesn't validate options - it passes through invalid values
// This test documents actual behavior, not expected error handling
mockGraphql.mockResolvedValueOnce({
node: {
field: {
id: 'FIELD_ID',
name: 'Status',
dataType: 'SINGLE_SELECT',
options: [
{ id: 'OPTION_1', name: 'To Do' },
{ id: 'OPTION_2', name: 'In Progress' }
]
}
}
});
mockGraphql.mockResolvedValueOnce({
updateProjectV2ItemFieldValue: {
projectV2Item: { id: 'ITEM_ID' }
}
});
// Service doesn't throw for invalid options - it passes through the value
const result = await service.setFieldValue({
...mockFieldData,
value: 'Invalid Status'
});
expect(result.success).toBe(true);
});
});
describe('ITERATION field type', () => {
it('should set iteration field value successfully', async () => {
// Mock field query response for ITERATION field (first call)
mockGraphql.mockResolvedValueOnce({
node: {
field: {
id: 'FIELD_ID',
name: 'Sprint',
dataType: 'ITERATION'
}
}
});
// Mock update mutation response (second call)
mockGraphql.mockResolvedValueOnce({
updateProjectV2ItemFieldValue: {
projectV2Item: { id: 'ITEM_ID' }
}
});
const result = await service.setFieldValue({
...mockFieldData,
value: 'ITERATION_ID_123'
});
expect(result).toEqual({
success: true,
message: "Field Sprint updated successfully"
});
expect(mockGraphql).toHaveBeenCalledTimes(2);
});
it('should handle iteration value object', async () => {
mockGraphql.mockResolvedValueOnce({
node: {
field: {
id: 'FIELD_ID',
name: 'Sprint',
dataType: 'ITERATION'
}
}
});
mockGraphql.mockResolvedValueOnce({
updateProjectV2ItemFieldValue: {
projectV2Item: { id: 'ITEM_ID' }
}
});
const result = await service.setFieldValue({
...mockFieldData,
value: { iterationId: 'ITERATION_123' }
});
expect(result.success).toBe(true);
});
});
describe('Error handling', () => {
it('should throw error for field not found', async () => {
mockGraphql.mockResolvedValueOnce({
node: { field: null }
});
await expect(service.setFieldValue({
...mockFieldData,
value: 'test'
})).rejects.toThrow(DomainError);
});
it('should throw error for unsupported field type', async () => {
mockGraphql.mockResolvedValueOnce({
node: {
field: {
id: 'FIELD_ID',
name: 'Unknown Field',
dataType: 'UNKNOWN_TYPE'
}
}
});
await expect(service.setFieldValue({
...mockFieldData,
value: 'test'
})).rejects.toThrow(DomainError);
});
});
});
describe('getFieldValue', () => {
const mockFieldData = {
projectId: 'PROJECT_ID',
itemId: 'ITEM_ID',
fieldId: 'FIELD_ID'
};
it('should get text field value successfully', async () => {
mockGraphql.mockResolvedValueOnce({
node: {
fieldValues: {
nodes: [
{
field: { id: 'FIELD_ID', name: 'Description' },
text: 'Sample text'
}
]
}
}
});
const result = await service.getFieldValue(mockFieldData);
// Service returns fieldId, fieldName, value, type format
expect(result).toEqual({
fieldId: 'FIELD_ID',
fieldName: 'Description',
value: 'Sample text',
type: 'TEXT'
});
});
it('should get number field value successfully', async () => {
mockGraphql.mockResolvedValueOnce({
node: {
fieldValues: {
nodes: [
{
field: { id: 'FIELD_ID', name: 'Story Points' },
number: 8
}
]
}
}
});
const result = await service.getFieldValue(mockFieldData);
expect(result).toEqual({
fieldId: 'FIELD_ID',
fieldName: 'Story Points',
value: 8,
type: 'NUMBER'
});
});
it('should get date field value successfully', async () => {
mockGraphql.mockResolvedValueOnce({
node: {
fieldValues: {
nodes: [
{
field: { id: 'FIELD_ID', name: 'Due Date' },
date: '2024-12-31'
}
]
}
}
});
const result = await service.getFieldValue(mockFieldData);
expect(result).toEqual({
fieldId: 'FIELD_ID',
fieldName: 'Due Date',
value: '2024-12-31',
type: 'DATE'
});
});
it('should get single select field value successfully', async () => {
mockGraphql.mockResolvedValueOnce({
node: {
fieldValues: {
nodes: [
{
field: { id: 'FIELD_ID', name: 'Status' },
name: 'In Progress',
optionId: 'OPTION_2'
}
]
}
}
});
const result = await service.getFieldValue(mockFieldData);
// Service returns { optionId, name } object for SINGLE_SELECT
expect(result).toEqual({
fieldId: 'FIELD_ID',
fieldName: 'Status',
value: { optionId: 'OPTION_2', name: 'In Progress' },
type: 'SINGLE_SELECT'
});
});
it('should get iteration field value successfully', async () => {
mockGraphql.mockResolvedValueOnce({
node: {
fieldValues: {
nodes: [
{
field: { id: 'FIELD_ID', name: 'Sprint' },
title: 'Sprint 1',
iterationId: 'ITERATION_123'
}
]
}
}
});
const result = await service.getFieldValue(mockFieldData);
expect(result).toEqual({
fieldId: 'FIELD_ID',
fieldName: 'Sprint',
value: { title: 'Sprint 1', iterationId: 'ITERATION_123' },
type: 'ITERATION'
});
});
it('should return null value when field not found in item', async () => {
mockGraphql.mockResolvedValueOnce({
node: {
fieldValues: {
nodes: [
{
field: { id: 'OTHER_FIELD', name: 'Other' },
text: 'Some value'
}
]
}
}
});
const result = await service.getFieldValue(mockFieldData);
expect(result).toEqual({
fieldId: 'FIELD_ID',
fieldName: 'unknown',
value: null,
type: 'unknown'
});
});
it('should throw error when item not found', async () => {
mockGraphql.mockResolvedValueOnce({
node: null
});
await expect(service.getFieldValue(mockFieldData)).rejects.toThrow(DomainError);
});
});
});