25개 이상의 토픽을 선택하실 수 없습니다. Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

284 lines
9.6KB

  1. import { describe, expect, it } from 'vitest'
  2. import {
  3. createMunicipalityProfile,
  4. createMunicipalityContact,
  5. deleteMunicipalityContact,
  6. fetchAvailableJurisdictions,
  7. fetchMunicipalityContacts,
  8. fetchMunicipalityProfiles,
  9. fetchPriorCycleDefaults,
  10. MunicipalityContactValidationError,
  11. MunicipalityValidationError,
  12. updateMunicipalityContact,
  13. updateMunicipalityProfile,
  14. type MunicipalityContact,
  15. type MunicipalityProfile,
  16. type PriorCycleDefaults,
  17. } from './municipalityContracts'
  18. const makeProfile = (overrides: Partial<MunicipalityProfile> = {}): MunicipalityProfile => ({
  19. profileId: 'abc123',
  20. jCode: 'FAIR01',
  21. displayName: 'Fairview Borough',
  22. updatedAt: '2026-05-06T12:00:00Z',
  23. updatedBy: 'user@test.com',
  24. legacyName: 'Fairview Borough',
  25. legacyMailingAddress: '100 Main St',
  26. legacyCityStateZip: 'Fairview, PA 16415',
  27. ...overrides,
  28. })
  29. const makeContact = (overrides: Partial<MunicipalityContact> = {}): MunicipalityContact => ({
  30. contactId: 'contact-1',
  31. profileId: 'abc123',
  32. contactType: 'Primary',
  33. name: 'Ada Clerk',
  34. roleTitle: 'Town Clerk',
  35. phone: '555-0100',
  36. email: 'ada@example.test',
  37. createdAt: '2026-05-07T12:00:00Z',
  38. createdBy: 'creator@example.test',
  39. updatedAt: '2026-05-07T12:00:00Z',
  40. updatedBy: 'actor@example.test',
  41. ...overrides,
  42. })
  43. const makeDefaults = (overrides: Partial<PriorCycleDefaults> = {}): PriorCycleDefaults => ({
  44. profileId: 'abc123',
  45. hasPriorCycles: true,
  46. selectedCycleId: 'cycle-2026-primary',
  47. emptyStateMessage: '',
  48. cycles: [
  49. {
  50. cycleId: 'cycle-2026-primary',
  51. cycleName: '2026 Primary',
  52. completedAt: '2026-05-01T00:00:00Z',
  53. services: [
  54. {
  55. serviceType: 'Addressing',
  56. summary: 'Standard addressing run',
  57. values: { Quantity: '1200', Tracking: 'Yes' },
  58. },
  59. ],
  60. },
  61. ],
  62. ...overrides,
  63. })
  64. // ── fetchMunicipalityProfiles ─────────────────────────────────────────────────
  65. describe('fetchMunicipalityProfiles', () => {
  66. it('returns profiles on 200', async () => {
  67. const stub = async () =>
  68. new Response(JSON.stringify([makeProfile()]), { status: 200 })
  69. const result = await fetchMunicipalityProfiles(stub)
  70. expect(result).toHaveLength(1)
  71. expect(result[0].jCode).toBe('FAIR01')
  72. expect(result[0].legacyName).toBe('Fairview Borough')
  73. })
  74. it('throws on non-200', async () => {
  75. const stub = async () => new Response('{}', { status: 500 })
  76. await expect(fetchMunicipalityProfiles(stub)).rejects.toThrow('500')
  77. })
  78. })
  79. // -- municipality contacts ---------------------------------------------------
  80. describe('municipality contact contracts', () => {
  81. it('fetchMunicipalityContacts returns primary and secondary contacts on 200', async () => {
  82. const stub = async () =>
  83. new Response(JSON.stringify([
  84. makeContact({ contactType: 'Primary', name: 'Main Clerk' }),
  85. makeContact({ contactId: 'contact-2', contactType: 'Secondary', name: 'Backup Clerk' }),
  86. ]), { status: 200 })
  87. const result = await fetchMunicipalityContacts('abc123', stub)
  88. expect(result.map((c) => c.contactType)).toEqual(['Primary', 'Secondary'])
  89. expect(result[0].name).toBe('Main Clerk')
  90. })
  91. it('createMunicipalityContact posts required and optional fields', async () => {
  92. let postedBody: unknown
  93. const stub = async (_input: RequestInfo | URL, init?: RequestInit) => {
  94. postedBody = JSON.parse(String(init?.body))
  95. return new Response(JSON.stringify(makeContact()), { status: 201 })
  96. }
  97. const result = await createMunicipalityContact(
  98. 'abc123',
  99. {
  100. contactType: 'Primary',
  101. name: 'Ada Clerk',
  102. roleTitle: 'Town Clerk',
  103. phone: '555-0100',
  104. email: 'ada@example.test',
  105. },
  106. stub,
  107. )
  108. expect(result.contactType).toBe('Primary')
  109. expect(postedBody).toEqual({
  110. contactType: 'Primary',
  111. name: 'Ada Clerk',
  112. roleTitle: 'Town Clerk',
  113. phone: '555-0100',
  114. email: 'ada@example.test',
  115. })
  116. })
  117. it('updateMunicipalityContact throws validation error for 422', async () => {
  118. const stub = async () =>
  119. new Response(JSON.stringify({ error: 'Name is required.' }), { status: 422 })
  120. await expect(
  121. updateMunicipalityContact('abc123', 'contact-1', { contactType: 'Primary', name: '' }, stub),
  122. ).rejects.toSatisfy(
  123. (e) => e instanceof MunicipalityContactValidationError && e.message.includes('Name'),
  124. )
  125. })
  126. it('deleteMunicipalityContact succeeds on 204 and throws on failure', async () => {
  127. const okStub = async () => new Response(null, { status: 204 })
  128. const failStub = async () => new Response('{}', { status: 500 })
  129. await expect(deleteMunicipalityContact('abc123', 'contact-1', okStub)).resolves.toBeUndefined()
  130. await expect(deleteMunicipalityContact('abc123', 'contact-1', failStub)).rejects.toThrow('500')
  131. })
  132. })
  133. describe('fetchPriorCycleDefaults', () => {
  134. it('returns read-only prior cycle defaults on 200', async () => {
  135. const stub = async () =>
  136. new Response(JSON.stringify(makeDefaults()), { status: 200 })
  137. const result = await fetchPriorCycleDefaults('abc123', stub)
  138. expect(result.hasPriorCycles).toBe(true)
  139. expect(result.selectedCycleId).toBe('cycle-2026-primary')
  140. expect(result.cycles[0].services[0].values.Quantity).toBe('1200')
  141. })
  142. it('returns empty state payload when no prior cycles exist', async () => {
  143. const stub = async () =>
  144. new Response(JSON.stringify(makeDefaults({
  145. hasPriorCycles: false,
  146. selectedCycleId: null,
  147. emptyStateMessage: 'No prior cycle defaults available.',
  148. cycles: [],
  149. })), { status: 200 })
  150. const result = await fetchPriorCycleDefaults('abc123', stub)
  151. expect(result.hasPriorCycles).toBe(false)
  152. expect(result.emptyStateMessage).toContain('No prior cycle')
  153. expect(result.cycles).toEqual([])
  154. })
  155. it('throws on non-200', async () => {
  156. const stub = async () => new Response('{}', { status: 404 })
  157. await expect(fetchPriorCycleDefaults('missing', stub)).rejects.toThrow('404')
  158. })
  159. })
  160. // ── createMunicipalityProfile ─────────────────────────────────────────────────
  161. describe('createMunicipalityProfile', () => {
  162. it('returns profile on 200', async () => {
  163. const stub = async () =>
  164. new Response(JSON.stringify(makeProfile()), { status: 200 })
  165. const result = await createMunicipalityProfile('FAIR01', 'Fairview', stub)
  166. expect(result.profileId).toBe('abc123')
  167. expect(result.jCode).toBe('FAIR01')
  168. })
  169. it('throws MunicipalityValidationError on 422 with descriptive message', async () => {
  170. const stub = async () =>
  171. new Response(JSON.stringify({ error: "No legacy jurisdiction found for JCode 'NOPE'." }), {
  172. status: 422,
  173. })
  174. await expect(createMunicipalityProfile('NOPE', null, stub)).rejects.toSatisfy(
  175. (e) => e instanceof MunicipalityValidationError && e.message.includes('NOPE'),
  176. )
  177. })
  178. it('throws generic Error on other non-200 status', async () => {
  179. const stub = async () => new Response('{}', { status: 500 })
  180. await expect(createMunicipalityProfile('FAIR01', null, stub)).rejects.toThrow('500')
  181. await expect(createMunicipalityProfile('FAIR01', null, stub)).rejects.not.toSatisfy(
  182. (e) => e instanceof MunicipalityValidationError,
  183. )
  184. })
  185. })
  186. // ── updateMunicipalityProfile ─────────────────────────────────────────────────
  187. describe('updateMunicipalityProfile', () => {
  188. it('returns updated profile on 200', async () => {
  189. const stub = async () =>
  190. new Response(JSON.stringify(makeProfile({ displayName: 'New Name' })), { status: 200 })
  191. const result = await updateMunicipalityProfile('abc123', 'New Name', stub)
  192. expect(result.displayName).toBe('New Name')
  193. })
  194. it('throws MunicipalityValidationError on 422', async () => {
  195. const stub = async () =>
  196. new Response(JSON.stringify({ error: 'Profile not found.' }), { status: 422 })
  197. await expect(updateMunicipalityProfile('ghost', 'X', stub)).rejects.toSatisfy(
  198. (e) => e instanceof MunicipalityValidationError,
  199. )
  200. })
  201. })
  202. // ── fetchAvailableJurisdictions ───────────────────────────────────────────────
  203. describe('fetchAvailableJurisdictions', () => {
  204. it('returns jurisdictions on 200', async () => {
  205. const stub = async () =>
  206. new Response(
  207. JSON.stringify([
  208. { jCode: 'FAIR01', name: 'Fairview Borough' },
  209. { jCode: 'LAKE02', name: null },
  210. ]),
  211. { status: 200 },
  212. )
  213. const result = await fetchAvailableJurisdictions(stub)
  214. expect(result).toHaveLength(2)
  215. expect(result[0].jCode).toBe('FAIR01')
  216. expect(result[0].name).toBe('Fairview Borough')
  217. expect(result[1].name).toBeNull()
  218. })
  219. it('throws on non-200', async () => {
  220. const stub = async () => new Response('{}', { status: 503 })
  221. await expect(fetchAvailableJurisdictions(stub)).rejects.toThrow('503')
  222. })
  223. })
  224. // ── MunicipalityValidationError ───────────────────────────────────────────────
  225. describe('MunicipalityValidationError', () => {
  226. it('has correct name and message', () => {
  227. const err = new MunicipalityValidationError('JCode not found')
  228. expect(err.name).toBe('MunicipalityValidationError')
  229. expect(err.message).toBe('JCode not found')
  230. expect(err).toBeInstanceOf(Error)
  231. })
  232. })

Powered by TurnKey Linux.