first commit
This commit is contained in:
157
libs/api/tracker/domain/src/lib/issue/issue.spec.ts
Normal file
157
libs/api/tracker/domain/src/lib/issue/issue.spec.ts
Normal file
@@ -0,0 +1,157 @@
|
||||
import {
|
||||
BusinessRuleError,
|
||||
ValidationError,
|
||||
} from '@task-stream/api/shared/domain';
|
||||
import { IssueCreatedEvent } from '../events/issue-created.event';
|
||||
import { IssueStatusChangedEvent } from '../events/issue-status-changed.event';
|
||||
import { IssueFactory } from '../factories/issue.factory';
|
||||
import { replay } from '../testing';
|
||||
import { Issue } from './issue';
|
||||
import { IssueLinkType, IssueStatus, IssueType } from './issue.enums';
|
||||
|
||||
const factory = new IssueFactory();
|
||||
|
||||
const newIssue = () =>
|
||||
factory.create({
|
||||
projectId: 'p-1',
|
||||
projectKey: 'TS',
|
||||
number: 7,
|
||||
title: ' Login button misaligned ',
|
||||
type: IssueType.UI_CHANGE,
|
||||
reporterId: 'u-qa',
|
||||
figmaUrl: 'https://www.figma.com/file/abc',
|
||||
labels: ['UI', 'ui ', 'login'],
|
||||
});
|
||||
|
||||
const moveTo = (issue: Issue, ...statuses: IssueStatus[]) =>
|
||||
statuses.forEach((s) => issue.changeStatus('u-dev', s, 'because'));
|
||||
|
||||
describe('Issue', () => {
|
||||
it('is created in BACKLOG with a project-scoped key and a creation event', () => {
|
||||
const issue = newIssue();
|
||||
const events = issue.getUncommittedEvents();
|
||||
|
||||
expect(issue.key).toBe('TS-7');
|
||||
expect(issue.status).toBe(IssueStatus.BACKLOG);
|
||||
expect(issue.title).toBe('Login button misaligned');
|
||||
expect(issue.labels).toEqual(['ui', 'login']);
|
||||
expect(events).toHaveLength(1);
|
||||
expect(events[0]).toBeInstanceOf(IssueCreatedEvent);
|
||||
});
|
||||
|
||||
it('rejects non-Figma design links', () => {
|
||||
expect(() =>
|
||||
factory.create({
|
||||
projectId: 'p-1',
|
||||
projectKey: 'TS',
|
||||
number: 1,
|
||||
title: 'x',
|
||||
type: IssueType.UI_CHANGE,
|
||||
reporterId: 'u',
|
||||
figmaUrl: 'https://evil.com/x',
|
||||
}),
|
||||
).toThrow(ValidationError);
|
||||
});
|
||||
|
||||
it('follows the QA workflow', () => {
|
||||
const issue = newIssue();
|
||||
moveTo(
|
||||
issue,
|
||||
IssueStatus.TODO,
|
||||
IssueStatus.IN_PROGRESS,
|
||||
IssueStatus.READY_FOR_QA,
|
||||
IssueStatus.IN_QA,
|
||||
);
|
||||
issue.changeStatus(
|
||||
'u-qa',
|
||||
IssueStatus.QA_FAILED,
|
||||
'Still 2px off on Safari',
|
||||
);
|
||||
moveTo(
|
||||
issue,
|
||||
IssueStatus.IN_PROGRESS,
|
||||
IssueStatus.READY_FOR_QA,
|
||||
IssueStatus.IN_QA,
|
||||
IssueStatus.DONE,
|
||||
);
|
||||
|
||||
expect(issue.status).toBe(IssueStatus.DONE);
|
||||
});
|
||||
|
||||
it('forbids skipping QA and requires a reason when QA fails', () => {
|
||||
const issue = newIssue();
|
||||
moveTo(issue, IssueStatus.TODO);
|
||||
expect(() => issue.changeStatus('u', IssueStatus.DONE)).toThrow(
|
||||
BusinessRuleError,
|
||||
);
|
||||
|
||||
moveTo(
|
||||
issue,
|
||||
IssueStatus.IN_PROGRESS,
|
||||
IssueStatus.READY_FOR_QA,
|
||||
IssueStatus.IN_QA,
|
||||
);
|
||||
expect(() =>
|
||||
issue.changeStatus('u-qa', IssueStatus.QA_FAILED, ' '),
|
||||
).toThrow(/reason/);
|
||||
});
|
||||
|
||||
it('links tickets once and never to itself', () => {
|
||||
const issue = newIssue();
|
||||
issue.linkTo('u', IssueLinkType.DEPENDS_ON, { id: 'i-be', key: 'BE-3' });
|
||||
|
||||
expect(() =>
|
||||
issue.linkTo('u', IssueLinkType.DEPENDS_ON, { id: 'i-be', key: 'BE-3' }),
|
||||
).toThrow(BusinessRuleError);
|
||||
expect(() =>
|
||||
issue.linkTo('u', IssueLinkType.RELATES_TO, {
|
||||
id: issue.id,
|
||||
key: 'TS-7',
|
||||
}),
|
||||
).toThrow(BusinessRuleError);
|
||||
|
||||
issue.unlink('u', IssueLinkType.DEPENDS_ON, 'i-be');
|
||||
expect(issue.links).toHaveLength(0);
|
||||
});
|
||||
|
||||
it('records events only for real changes', () => {
|
||||
const issue = newIssue();
|
||||
issue.uncommit();
|
||||
issue.updateDetails('u', {
|
||||
title: 'Login button misaligned',
|
||||
labels: ['login', 'ui'],
|
||||
});
|
||||
issue.updateDetails('u', { priority: issue.priority });
|
||||
|
||||
expect(issue.getUncommittedEvents()).toHaveLength(1);
|
||||
});
|
||||
|
||||
it('is rebuilt from its event history (event sourcing)', () => {
|
||||
const issue = newIssue();
|
||||
moveTo(issue, IssueStatus.TODO, IssueStatus.IN_PROGRESS);
|
||||
issue.assign('u-qa', 'u-dev');
|
||||
issue.linkTo('u', IssueLinkType.TESTS, { id: 'i-2', key: 'TS-2' });
|
||||
|
||||
const { rebuilt, history } = replay(issue, Issue);
|
||||
|
||||
expect(history.map((e) => e.type)).toEqual([
|
||||
'IssueCreatedEvent',
|
||||
'IssueStatusChangedEvent',
|
||||
'IssueStatusChangedEvent',
|
||||
'IssueAssignedEvent',
|
||||
'IssueLinkedEvent',
|
||||
]);
|
||||
expect(history[1].data).toBeInstanceOf(IssueStatusChangedEvent);
|
||||
expect(rebuilt.version.value).toBe(5);
|
||||
expect(rebuilt).toMatchObject({
|
||||
key: 'TS-7',
|
||||
status: IssueStatus.IN_PROGRESS,
|
||||
assigneeId: 'u-dev',
|
||||
labels: ['ui', 'login'],
|
||||
links: [
|
||||
{ type: IssueLinkType.TESTS, targetIssueId: 'i-2', targetKey: 'TS-2' },
|
||||
],
|
||||
});
|
||||
expect(rebuilt.getUncommittedEvents()).toHaveLength(0);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user