diff --git a/src/infrastructure/jira/jira-service.test.ts b/src/infrastructure/jira/jira-service.test.ts index 8d17e2f3..557f425c 100644 --- a/src/infrastructure/jira/jira-service.test.ts +++ b/src/infrastructure/jira/jira-service.test.ts @@ -13,9 +13,10 @@ import { import type { AttachedDesignUrlV2IssuePropertyValue } from './jira-service'; import { ConfigurationState, + IssueNotFoundJiraServiceError, issuePropertyKeys, - JiraService, jiraService, + JiraService, SubmitDesignJiraServiceError, } from './jira-service'; @@ -247,6 +248,18 @@ describe('JiraService', () => { connectInstallation, ); }); + + it('should throw an error if issue is not found', async () => { + const connectInstallation = generateConnectInstallation(); + const jiraIssue = generateJiraIssue(); + jest + .spyOn(jiraClient, 'getIssue') + .mockRejectedValue(new NotFoundHttpClientError()); + + await expect(() => + jiraService.getIssue(jiraIssue.key, connectInstallation), + ).rejects.toThrow(IssueNotFoundJiraServiceError); + }); }); describe('setAttachedDesignUrlInIssuePropertiesIfMissing', () => { diff --git a/src/infrastructure/jira/jira-service.ts b/src/infrastructure/jira/jira-service.ts index 8b4e4eff..2813308b 100644 --- a/src/infrastructure/jira/jira-service.ts +++ b/src/infrastructure/jira/jira-service.ts @@ -94,7 +94,14 @@ export class JiraService { issueIdOrKey: string, connectInstallation: ConnectInstallation, ): Promise => { - return await jiraClient.getIssue(issueIdOrKey, connectInstallation); + try { + return await jiraClient.getIssue(issueIdOrKey, connectInstallation); + } catch (err) { + if (err instanceof NotFoundHttpClientError) { + throw new IssueNotFoundJiraServiceError('Issue not found'); + } + throw err; + } }; saveDesignUrlInIssueProperties = async ( @@ -427,6 +434,8 @@ export class JiraService { export const jiraService = new JiraService(); +export class IssueNotFoundJiraServiceError extends CauseAwareError {} + export class SubmitDesignJiraServiceError extends CauseAwareError { designId?: string; rejectionErrors?: { readonly message: string }[]; diff --git a/src/usecases/associate-design-use-case.test.ts b/src/usecases/associate-design-use-case.test.ts index c4df9a69..7a7d4a11 100644 --- a/src/usecases/associate-design-use-case.test.ts +++ b/src/usecases/associate-design-use-case.test.ts @@ -3,6 +3,7 @@ import { associateDesignUseCase } from './associate-design-use-case'; import { FigmaDesignNotFoundUseCaseResultError, InvalidInputUseCaseResultError, + JiraIssueNotFoundUseCaseResultError, } from './errors'; import { generateAssociateDesignUseCaseParams } from './testing'; @@ -19,7 +20,10 @@ import { generateJiraIssue, } from '../domain/entities/testing'; import { figmaService } from '../infrastructure/figma'; -import { jiraService } from '../infrastructure/jira'; +import { + IssueNotFoundJiraServiceError, + jiraService, +} from '../infrastructure/jira'; import { associatedFigmaDesignRepository } from '../infrastructure/repositories'; describe('associateDesignUseCase', () => { @@ -133,6 +137,26 @@ describe('associateDesignUseCase', () => { ).rejects.toBeInstanceOf(InvalidInputUseCaseResultError); }); + it('should throw JiraIssueNotFoundUseCaseResultError when the issue is not found', async () => { + const connectInstallation = generateConnectInstallation(); + const issue = generateJiraIssue(); + const fileKey = generateFigmaFileKey(); + const params: AssociateDesignUseCaseParams = + generateAssociateDesignUseCaseParams({ + designUrl: generateFigmaDesignUrl({ fileKey }), + issueId: issue.id, + connectInstallation, + }); + jest.spyOn(figmaService, 'getDesignOrParent').mockResolvedValue(null); + jest + .spyOn(jiraService, 'getIssue') + .mockRejectedValue(new IssueNotFoundJiraServiceError()); + + await expect(() => + associateDesignUseCase.execute(params), + ).rejects.toBeInstanceOf(JiraIssueNotFoundUseCaseResultError); + }); + it('should not save associated Figma design when design submission fails', async () => { const connectInstallation = generateConnectInstallation(); const issue = generateJiraIssue(); diff --git a/src/usecases/associate-design-use-case.ts b/src/usecases/associate-design-use-case.ts index 220c609e..e8698ec0 100644 --- a/src/usecases/associate-design-use-case.ts +++ b/src/usecases/associate-design-use-case.ts @@ -2,6 +2,7 @@ import { FigmaDesignNotFoundUseCaseResultError, ForbiddenByFigmaUseCaseResultError, InvalidInputUseCaseResultError, + JiraIssueNotFoundUseCaseResultError, } from './errors'; import type { AtlassianEntity } from './types'; @@ -15,7 +16,10 @@ import { figmaService, UnauthorizedFigmaServiceError, } from '../infrastructure/figma'; -import { jiraService } from '../infrastructure/jira'; +import { + IssueNotFoundJiraServiceError, + jiraService, +} from '../infrastructure/jira'; import { associatedFigmaDesignRepository } from '../infrastructure/repositories'; export type AssociateDesignUseCaseParams = { @@ -99,6 +103,9 @@ export const associateDesignUseCase = { if (e instanceof UnauthorizedFigmaServiceError) { throw new ForbiddenByFigmaUseCaseResultError(e); } + if (e instanceof IssueNotFoundJiraServiceError) { + throw new JiraIssueNotFoundUseCaseResultError(e); + } throw e; } diff --git a/src/usecases/backfill-design-use-case.test.ts b/src/usecases/backfill-design-use-case.test.ts index 41bb63b9..d93dcf5e 100644 --- a/src/usecases/backfill-design-use-case.test.ts +++ b/src/usecases/backfill-design-use-case.test.ts @@ -1,6 +1,9 @@ import type { BackfillDesignUseCaseParams } from './backfill-design-use-case'; import { backfillDesignUseCase } from './backfill-design-use-case'; -import { InvalidInputUseCaseResultError } from './errors'; +import { + InvalidInputUseCaseResultError, + JiraIssueNotFoundUseCaseResultError, +} from './errors'; import { generateBackfillDesignUseCaseParams } from './testing'; import type { AssociatedFigmaDesign } from '../domain/entities'; @@ -17,7 +20,10 @@ import { } from '../domain/entities/testing'; import { figmaService } from '../infrastructure/figma'; import { figmaBackfillService } from '../infrastructure/figma/figma-backfill-service'; -import { jiraService } from '../infrastructure/jira'; +import { + IssueNotFoundJiraServiceError, + jiraService, +} from '../infrastructure/jira'; import { associatedFigmaDesignRepository } from '../infrastructure/repositories'; describe('backfillDesignUseCase', () => { @@ -215,4 +221,41 @@ describe('backfillDesignUseCase', () => { ); expect(associatedFigmaDesignRepository.upsert).not.toHaveBeenCalled(); }); + + it('should throw JiraIssueNotFoundUseCaseResultError when the issue is not found', async () => { + const connectInstallation = generateConnectInstallation(); + const issue = generateJiraIssue(); + const fileKey = generateFigmaFileKey(); + const designId = new FigmaDesignIdentifier(fileKey); + const atlassianDesign = generateAtlassianDesign({ + id: designId.toAtlassianDesignId(), + }); + + const params: BackfillDesignUseCaseParams = + generateBackfillDesignUseCaseParams({ + designUrl: generateFigmaDesignUrl({ fileKey }), + issueId: issue.id, + connectInstallation, + }); + + jest + .spyOn(figmaService, 'getDesignOrParent') + .mockResolvedValue(atlassianDesign); + jest.spyOn(jiraService, 'getIssue').mockResolvedValue(issue); + jest + .spyOn(jiraService, 'submitDesign') + .mockRejectedValue(new IssueNotFoundJiraServiceError()); + jest + .spyOn(jiraService, 'saveDesignUrlInIssueProperties') + .mockResolvedValue(); + jest + .spyOn(figmaService, 'tryCreateDevResourceForJiraIssue') + .mockResolvedValue(); + jest.spyOn(associatedFigmaDesignRepository, 'upsert'); + + await expect(() => backfillDesignUseCase.execute(params)).rejects.toThrow( + JiraIssueNotFoundUseCaseResultError, + ); + expect(associatedFigmaDesignRepository.upsert).not.toHaveBeenCalled(); + }); }); diff --git a/src/usecases/backfill-design-use-case.ts b/src/usecases/backfill-design-use-case.ts index 310bcb03..1a61bec7 100644 --- a/src/usecases/backfill-design-use-case.ts +++ b/src/usecases/backfill-design-use-case.ts @@ -1,6 +1,7 @@ import { ForbiddenByFigmaUseCaseResultError, InvalidInputUseCaseResultError, + JiraIssueNotFoundUseCaseResultError, } from './errors'; import type { AtlassianEntity } from './types'; @@ -15,7 +16,10 @@ import { UnauthorizedFigmaServiceError, } from '../infrastructure/figma'; import { figmaBackfillService } from '../infrastructure/figma/figma-backfill-service'; -import { jiraService } from '../infrastructure/jira'; +import { + IssueNotFoundJiraServiceError, + jiraService, +} from '../infrastructure/jira'; import { associatedFigmaDesignRepository } from '../infrastructure/repositories'; export type BackfillDesignUseCaseParams = { @@ -105,6 +109,9 @@ export const backfillDesignUseCase = { if (e instanceof UnauthorizedFigmaServiceError) { throw new ForbiddenByFigmaUseCaseResultError(e); } + if (e instanceof IssueNotFoundJiraServiceError) { + throw new JiraIssueNotFoundUseCaseResultError(e); + } throw e; } diff --git a/src/usecases/errors.ts b/src/usecases/errors.ts index 2b50ab32..a22f04ce 100644 --- a/src/usecases/errors.ts +++ b/src/usecases/errors.ts @@ -12,6 +12,12 @@ export class ForbiddenByFigmaUseCaseResultError extends UseCaseResultError { } } +export class JiraIssueNotFoundUseCaseResultError extends UseCaseResultError { + constructor(cause?: Error) { + super('Issue is not found.', cause); + } +} + export class FigmaDesignNotFoundUseCaseResultError extends UseCaseResultError { constructor(cause?: Error) { super('Design is not found.', cause); diff --git a/src/web/middleware/error-handler-middleware.ts b/src/web/middleware/error-handler-middleware.ts index a2ec53ab..59053164 100644 --- a/src/web/middleware/error-handler-middleware.ts +++ b/src/web/middleware/error-handler-middleware.ts @@ -5,6 +5,7 @@ import { FigmaDesignNotFoundUseCaseResultError, ForbiddenByFigmaUseCaseResultError, InvalidInputUseCaseResultError, + JiraIssueNotFoundUseCaseResultError, PaidFigmaPlanRequiredUseCaseResultError, UseCaseResultError, } from '../../usecases'; @@ -49,7 +50,10 @@ export const errorHandlerMiddleware = ( return next(); } - if (err instanceof FigmaDesignNotFoundUseCaseResultError) { + if ( + err instanceof FigmaDesignNotFoundUseCaseResultError || + err instanceof JiraIssueNotFoundUseCaseResultError + ) { res.status(HttpStatusCode.NotFound).send({ message: err.message }); return next(); }