eletrotupi / tcc/ commit / b444500

api: unit-test the time, token, and validation helpers

Adds focused unit tests for the generic formatting helpers, the
app-timezone day-window helpers, the token expiry helpers, and the
validation-error formatter, covering the boundaries each one turns on
Pedro Lucas Porcellis porcellis@eletrotupi.com · 10 days ago b44450051fd5637c480a9dec8ae1f93921d1754c Browse files
Parents 7d05736
Changed files
4
Additions
+192
Deletions
0
/dev/null → api/tests/auth/jwt.unit.test.ts
1 +
import { describe, it, expect } from 'vitest'
2 +
import { isTokenExpired, getPasswordResetExpiration } from '@app/lib/jwt'
3 +

4 +
describe('isTokenExpired', () => {
5 +
  it('treats a past date as expired', () => {
6 +
    expect(isTokenExpired(new Date(Date.now() - 60_000))).toBe(true)
7 +
  })
8 +

9 +
  it('treats a future date as live', () => {
10 +
    expect(isTokenExpired(new Date(Date.now() + 60_000))).toBe(false)
11 +
  })
12 +
})
13 +

14 +
describe('getPasswordResetExpiration', () => {
15 +
  it('lands one hour ahead of now', () => {
16 +
    const oneHour = 60 * 60 * 1000
17 +
    const before = Date.now()
18 +
    const expiration = getPasswordResetExpiration()
19 +
    const after = Date.now()
20 +

21 +
    expect(expiration.getTime()).toBeGreaterThanOrEqual(before + oneHour)
22 +
    expect(expiration.getTime()).toBeLessThanOrEqual(after + oneHour)
23 +
  })
24 +
})
/dev/null → api/tests/errors/validation-error.unit.test.ts
1 +
import { describe, it, expect } from 'vitest'
2 +
import { z } from 'zod'
3 +
import { formatValidationError } from '@app/lib/errors/validationError'
4 +

5 +
describe('formatValidationError', () => {
6 +
  it('keys a single field by its name', () => {
7 +
    const schema = z.object({ email: z.string().email() })
8 +
    const parsed = schema.safeParse({ email: 'nope' })
9 +

10 +
    if (parsed.success) throw new Error('expected validation to fail')
11 +

12 +
    const { error, fields } = formatValidationError(parsed.error)
13 +

14 +
    expect(Object.keys(fields)).toEqual(['email'])
15 +
    expect(typeof fields.email).toBe('string')
16 +
    expect(fields.email.length).toBeGreaterThan(0)
17 +
    expect(error).toContain(fields.email)
18 +
  })
19 +

20 +
  it('joins the segments of a nested path with dots', () => {
21 +
    const schema = z.object({ profile: z.object({ age: z.number() }) })
22 +
    const parsed = schema.safeParse({ profile: { age: 'x' } })
23 +

24 +
    if (parsed.success) throw new Error('expected validation to fail')
25 +

26 +
    const { fields } = formatValidationError(parsed.error)
27 +

28 +
    expect(Object.keys(fields)).toEqual(['profile.age'])
29 +
  })
30 +

31 +
  it('keeps only the first message for a field', () => {
32 +
    const schema = z.object({ code: z.string().min(5).regex(/^a/) })
33 +
    const parsed = schema.safeParse({ code: 'b' })
34 +

35 +
    if (parsed.success) throw new Error('expected validation to fail')
36 +

37 +
    const { fields } = formatValidationError(parsed.error)
38 +

39 +
    expect(parsed.error.issues.length).toBeGreaterThan(1)
40 +
    expect(fields.code).toBe(parsed.error.issues[0].message)
41 +
    expect(fields.code).not.toBe(parsed.error.issues[1].message)
42 +
  })
43 +
})
/dev/null → api/tests/insights/app-day.unit.test.ts
1 +
import { describe, it, expect } from 'vitest'
2 +
import {
3 +
  startOfAppDay,
4 +
  previousAppDay,
5 +
  parseAppDay,
6 +
  appDayWindow,
7 +
  appDayPeriod,
8 +
} from '@app/lib/queue/processors/insights/utils'
9 +

10 +
// America/Sao_Paulo is UTC-3 all year (Brazil dropped daylight saving in 2019),
11 +
// so the app's local day starts at 03:00Z; the trailing 3 is that offset.
12 +
const saoPauloMidnight = (year: number, month: number, day: number) =>
13 +
  Date.UTC(year, month - 1, day, 3, 0, 0)
14 +

15 +
describe('startOfAppDay', () => {
16 +
  it('floors a mid-day instant to the start of the local day', () => {
17 +
    expect(startOfAppDay(new Date('2026-10-17T12:00:00.000Z')).getTime()).toBe(
18 +
      saoPauloMidnight(2026, 10, 17),
19 +
    )
20 +
  })
21 +

22 +
  it('keeps an instant just before local midnight on the same day', () => {
23 +
    expect(
24 +
      startOfAppDay(new Date('2026-10-18T02:59:59.999Z')).getTime(),
25 +
    ).toBe(saoPauloMidnight(2026, 10, 17))
26 +
  })
27 +

28 +
  it('rolls over to the next day at local midnight', () => {
29 +
    expect(
30 +
      startOfAppDay(new Date('2026-10-18T03:00:00.000Z')).getTime(),
31 +
    ).toBe(saoPauloMidnight(2026, 10, 18))
32 +
  })
33 +
})
34 +

35 +
describe('previousAppDay', () => {
36 +
  it('returns the start of the day before', () => {
37 +
    expect(previousAppDay(new Date('2026-10-17T12:00:00.000Z')).getTime()).toBe(
38 +
      saoPauloMidnight(2026, 10, 16),
39 +
    )
40 +
  })
41 +
})
42 +

43 +
describe('parseAppDay', () => {
44 +
  it('parses a day string at the local midnight', () => {
45 +
    expect(parseAppDay('2026-10-17').getTime()).toBe(
46 +
      saoPauloMidnight(2026, 10, 17),
47 +
    )
48 +
  })
49 +
})
50 +

51 +
describe('appDayWindow', () => {
52 +
  it('spans from one local day start to the next', () => {
53 +
    const { start, end } = appDayWindow(new Date('2026-10-17T12:00:00.000Z'))
54 +

55 +
    expect(start.getTime()).toBe(saoPauloMidnight(2026, 10, 17))
56 +
    expect(end.getTime()).toBe(saoPauloMidnight(2026, 10, 18))
57 +
  })
58 +

59 +
  it('rolls the whole window over at local midnight', () => {
60 +
    const before = appDayWindow(new Date('2026-10-18T02:59:59.999Z'))
61 +
    const after = appDayWindow(new Date('2026-10-18T03:00:00.000Z'))
62 +

63 +
    expect(before.start.getTime()).toBe(saoPauloMidnight(2026, 10, 17))
64 +
    expect(before.end.getTime()).toBe(saoPauloMidnight(2026, 10, 18))
65 +
    expect(after.start.getTime()).toBe(saoPauloMidnight(2026, 10, 18))
66 +
    expect(after.end.getTime()).toBe(saoPauloMidnight(2026, 10, 19))
67 +
  })
68 +
})
69 +

70 +
describe('appDayPeriod', () => {
71 +
  it('serializes the window as offset ISO strings', () => {
72 +
    expect(appDayPeriod(new Date('2026-10-17T12:00:00.000Z'))).toEqual({
73 +
      periodStart: '2026-10-17T00:00:00.000-03:00',
74 +
      periodEnd: '2026-10-18T00:00:00.000-03:00',
75 +
    })
76 +
  })
77 +
})
/dev/null → api/tests/utils/calc.unit.test.ts
1 +
import { describe, it, expect } from 'vitest'
2 +
import { avg, formatHours, formatDiff } from '@app/utils/calc'
3 +

4 +
describe('avg', () => {
5 +
  it('averages a list of numbers', () => {
6 +
    expect(avg([2, 4, 6])).toBe(4)
7 +
  })
8 +

9 +
  it('yields NaN for an empty list', () => {
10 +
    expect(avg([])).toBeNaN()
11 +
  })
12 +
})
13 +

14 +
describe('formatHours', () => {
15 +
  it('drops the minutes for a whole hour', () => {
16 +
    expect(formatHours(7)).toBe('7h')
17 +
  })
18 +

19 +
  it('renders the remaining minutes', () => {
20 +
    expect(formatHours(7.5)).toBe('7h 30min')
21 +
  })
22 +
})
23 +

24 +
describe('formatDiff', () => {
25 +
  it('signs a positive whole-hour difference', () => {
26 +
    expect(formatDiff(120)).toBe('+2h')
27 +
  })
28 +

29 +
  it('signs a negative whole-hour difference', () => {
30 +
    expect(formatDiff(-120)).toBe('-2h')
31 +
  })
32 +

33 +
  it('signs a positive difference with hours and minutes', () => {
34 +
    expect(formatDiff(90)).toBe('+1h 30min')
35 +
  })
36 +

37 +
  it('signs a negative difference with hours and minutes', () => {
38 +
    expect(formatDiff(-90)).toBe('-1h 30min')
39 +
  })
40 +

41 +
  it('renders a positive sub-hour difference in minutes only', () => {
42 +
    expect(formatDiff(30)).toBe('+30min')
43 +
  })
44 +

45 +
  it('renders a negative sub-hour difference in minutes only', () => {
46 +
    expect(formatDiff(-30)).toBe('-30min')
47 +
  })
48 +
})