| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136 |
- import {
- allowedNextStatuses,
- canReadTicket,
- canTransition,
- isSlaOverdue,
- resolveDeadline,
- slaStatus,
- validateAttachment
- } from '../src/services/quality-rules';
- describe('ticket status rules', () => {
- it('allows pending ticket to enter processing', () => {
- expect(canTransition('pending', 'processing')).toBe(true);
- });
- it('allows pending ticket to be cancelled', () => {
- expect(canTransition('pending', 'cancelled')).toBe(true);
- });
- it('rejects pending ticket resolved directly', () => {
- expect(canTransition('pending', 'resolved')).toBe(false);
- });
- it('allows processing ticket to submit customer confirmation', () => {
- expect(canTransition('processing', 'confirming')).toBe(true);
- });
- it('allows customer to return confirming ticket to processing', () => {
- expect(canTransition('confirming', 'processing')).toBe(true);
- });
- it('allows customer to resolve confirming ticket', () => {
- expect(canTransition('confirming', 'resolved')).toBe(true);
- });
- it('rejects closed ticket reopening through status machine', () => {
- expect(canTransition('closed', 'processing')).toBe(false);
- });
- it('returns immutable next status list', () => {
- const next = allowedNextStatuses('pending');
- next.push('closed');
- expect(allowedNextStatuses('pending')).toEqual(['processing', 'cancelled']);
- });
- });
- describe('permission rules', () => {
- const acl = {
- creatorId: 'customer-1',
- ownerId: 'engineer-1',
- companyUserIds: ['customer-1', 'customer-2']
- };
- it('allows admin to read every ticket', () => {
- expect(canReadTicket('admin-1', 'admin', acl)).toBe(true);
- });
- it('allows support to read every ticket', () => {
- expect(canReadTicket('support-1', 'support', acl)).toBe(true);
- });
- it('allows ticket owner engineer to read assigned ticket', () => {
- expect(canReadTicket('engineer-1', 'engineer', acl)).toBe(true);
- });
- it('rejects unrelated engineer', () => {
- expect(canReadTicket('engineer-2', 'engineer', acl)).toBe(false);
- });
- it('allows creator customer to read own ticket', () => {
- expect(canReadTicket('customer-1', 'customer', acl)).toBe(true);
- });
- it('allows same company customer to read company ticket', () => {
- expect(canReadTicket('customer-2', 'customer', acl)).toBe(true);
- });
- it('rejects unrelated customer', () => {
- expect(canReadTicket('customer-3', 'customer', acl)).toBe(false);
- });
- });
- describe('SLA rules', () => {
- const createdAt = new Date('2026-06-01T00:00:00.000Z');
- it('calculates urgent deadline within 4 hours', () => {
- expect(resolveDeadline({ priority: 'urgent', createdAt }).toISOString()).toBe('2026-06-01T04:00:00.000Z');
- });
- it('calculates high priority deadline within 24 hours', () => {
- expect(resolveDeadline({ priority: 'high', createdAt }).toISOString()).toBe('2026-06-02T00:00:00.000Z');
- });
- it('marks unresolved ticket overdue after deadline', () => {
- expect(isSlaOverdue({ priority: 'urgent', createdAt, now: new Date('2026-06-01T04:01:00.000Z') })).toBe(true);
- });
- it('keeps ticket normal before warning window', () => {
- expect(slaStatus({ priority: 'high', createdAt, now: new Date('2026-06-01T12:00:00.000Z') })).toBe('normal');
- });
- it('marks ticket warning in last 20 percent time window', () => {
- expect(slaStatus({ priority: 'high', createdAt, now: new Date('2026-06-01T20:00:00.000Z') })).toBe('warning');
- });
- it('marks ticket overdue when resolved after deadline', () => {
- expect(isSlaOverdue({
- priority: 'urgent',
- createdAt,
- now: new Date('2026-06-01T03:00:00.000Z'),
- resolvedAt: new Date('2026-06-01T04:30:00.000Z')
- })).toBe(true);
- });
- });
- describe('attachment validation rules', () => {
- it('accepts png attachment under 10MB', () => {
- expect(validateAttachment({ name: 'error.png', sizeMb: 2 }).valid).toBe(true);
- });
- it('accepts log attachment under 10MB', () => {
- expect(validateAttachment({ name: 'console.log', sizeMb: 0.5 }).valid).toBe(true);
- });
- it('rejects unsupported executable file', () => {
- expect(validateAttachment({ name: 'tool.exe', sizeMb: 1 })).toEqual({ valid: false, reason: 'UNSUPPORTED_TYPE' });
- });
- it('rejects empty file', () => {
- expect(validateAttachment({ name: 'empty.pdf', sizeMb: 0 })).toEqual({ valid: false, reason: 'EMPTY_FILE' });
- });
- it('rejects file larger than 10MB', () => {
- expect(validateAttachment({ name: 'large.pdf', sizeMb: 10.1 })).toEqual({ valid: false, reason: 'FILE_TOO_LARGE' });
- });
- });
|