11'use strict' ;
22
33const assert = require ( 'assert' ) ;
4+ const sinon = require ( 'sinon' ) ;
45const path = require ( 'path' ) ;
56
67require ( 'dotenv' ) . config ( { path : path . resolve ( __dirname , '../../../../../.env' ) } ) ;
78
9+ const Hubspot = require ( '../../Hubspot' ) ;
810const componentPath = '../../crm/UpdateCompany/UpdateCompany' ;
911const component = require ( componentPath ) ;
1012const { createMockContext } = require ( '../../../../../test/utils' ) ;
1113
1214describe ( 'HubSpot -> UpdateCompany' , ( ) => {
1315
1416 let context ;
17+ let hubspotCallStub ;
1518 const mockAccessToken = 'test-access-token' ;
1619
1720 beforeEach ( ( ) => {
@@ -20,9 +23,14 @@ describe('HubSpot -> UpdateCompany', () => {
2023 accessToken : mockAccessToken
2124 }
2225 } ) ;
26+ hubspotCallStub = sinon . stub ( Hubspot . prototype , 'call' ) ;
2327 } ) ;
2428
25- // Validation tests - these test important logic without requiring HTTP mocking
29+ afterEach ( ( ) => {
30+ sinon . restore ( ) ;
31+ } ) ;
32+
33+ // Validation tests
2634 it ( 'should throw error when neither companyId nor domain is provided' , async ( ) => {
2735 context . messages = {
2836 in : {
@@ -40,16 +48,254 @@ describe('HubSpot -> UpdateCompany', () => {
4048 }
4149 } ) ;
4250
43- // TODO: Add HTTP mocking tests
44- // The following tests require proper axios/HTTP mocking to work in CI environment:
45- // - should update company by companyId with merge strategy
46- // - should update company by domain (search first)
47- // - should update company with overwrite strategy
48- // - should handle no fields to update with merge strategy
49- // - should throw error when domain not found
50- // - should handle additionalProperties for custom fields (e.g. Clearbit fields)
51- //
52- // These components have been tested in staging environment with real HubSpot API.
53- // The merge strategy logic (only updating empty fields) has been verified to work correctly.
54- // HTTP mocking will be added after consulting AppMixer team on their preferred patterns.
51+ // HTTP mocking tests using sinon stubs on Hubspot.prototype.call
52+ it ( 'should update company by companyId with merge strategy' , async ( ) => {
53+ context . messages = {
54+ in : {
55+ content : {
56+ companyId : '123' ,
57+ name : 'New Name' ,
58+ industry : 'Technology'
59+ }
60+ }
61+ } ;
62+
63+ // First call: GET existing company (merge strategy fetches current data)
64+ hubspotCallStub . withArgs ( 'get' , 'crm/v3/objects/companies/123' ) . resolves ( {
65+ data : {
66+ id : '123' ,
67+ properties : {
68+ name : 'Existing Name' , // non-empty, should NOT be overwritten
69+ industry : '' // empty, should be updated
70+ }
71+ }
72+ } ) ;
73+
74+ // Second call: PATCH with only the empty fields
75+ hubspotCallStub . withArgs ( 'patch' , 'crm/v3/objects/companies/123' ) . resolves ( {
76+ data : {
77+ id : '123' ,
78+ properties : {
79+ name : 'Existing Name' ,
80+ industry : 'Technology'
81+ }
82+ }
83+ } ) ;
84+
85+ await component . receive ( context ) ;
86+
87+ // Verify PATCH was called with only the empty field (industry), not name
88+ const patchCall = hubspotCallStub . getCalls ( ) . find ( c => c . args [ 0 ] === 'patch' ) ;
89+ assert ( patchCall , 'PATCH should have been called' ) ;
90+ assert . strictEqual ( patchCall . args [ 2 ] . properties . industry , 'Technology' ) ;
91+ assert . strictEqual ( patchCall . args [ 2 ] . properties . name , undefined ) ;
92+
93+ // Verify sendJson called with updated: true
94+ assert ( context . sendJson . calledOnce , 'sendJson should be called once' ) ;
95+ const output = context . sendJson . getCall ( 0 ) . args [ 0 ] ;
96+ assert . strictEqual ( output . updated , true ) ;
97+ } ) ;
98+
99+ it ( 'should update company by domain (search first)' , async ( ) => {
100+ context . messages = {
101+ in : {
102+ content : {
103+ domain : 'example.com' ,
104+ name : 'New Name'
105+ }
106+ }
107+ } ;
108+
109+ // First call: POST search by domain
110+ hubspotCallStub . withArgs ( 'post' , 'crm/v3/objects/companies/search' ) . resolves ( {
111+ data : {
112+ results : [ { id : '456' } ]
113+ }
114+ } ) ;
115+
116+ // Second call: GET existing company for merge strategy
117+ hubspotCallStub . withArgs ( 'get' , 'crm/v3/objects/companies/456' ) . resolves ( {
118+ data : {
119+ id : '456' ,
120+ properties : {
121+ domain : 'example.com' ,
122+ name : '' // empty, should be updated
123+ }
124+ }
125+ } ) ;
126+
127+ // Third call: PATCH with updated fields
128+ hubspotCallStub . withArgs ( 'patch' , 'crm/v3/objects/companies/456' ) . resolves ( {
129+ data : {
130+ id : '456' ,
131+ properties : {
132+ domain : 'example.com' ,
133+ name : 'New Name'
134+ }
135+ }
136+ } ) ;
137+
138+ await component . receive ( context ) ;
139+
140+ // Verify search was called with correct domain filter
141+ const searchCall = hubspotCallStub . getCalls ( ) . find ( c => c . args [ 0 ] === 'post' ) ;
142+ assert ( searchCall , 'Search POST should have been called' ) ;
143+ const filters = searchCall . args [ 2 ] . filterGroups [ 0 ] . filters ;
144+ assert . strictEqual ( filters [ 0 ] . propertyName , 'domain' ) ;
145+ assert . strictEqual ( filters [ 0 ] . value , 'example.com' ) ;
146+
147+ // Verify sendJson called with updated: true
148+ assert ( context . sendJson . calledOnce , 'sendJson should be called once' ) ;
149+ const output = context . sendJson . getCall ( 0 ) . args [ 0 ] ;
150+ assert . strictEqual ( output . updated , true ) ;
151+ } ) ;
152+
153+ it ( 'should update company with overwrite strategy' , async ( ) => {
154+ context . messages = {
155+ in : {
156+ content : {
157+ companyId : '123' ,
158+ name : 'Overwritten Name' ,
159+ industry : 'Finance' ,
160+ updateStrategy : 'overwrite'
161+ }
162+ }
163+ } ;
164+
165+ // Overwrite strategy skips the GET call, goes straight to PATCH
166+ hubspotCallStub . withArgs ( 'patch' , 'crm/v3/objects/companies/123' ) . resolves ( {
167+ data : {
168+ id : '123' ,
169+ properties : {
170+ name : 'Overwritten Name' ,
171+ industry : 'Finance'
172+ }
173+ }
174+ } ) ;
175+
176+ await component . receive ( context ) ;
177+
178+ // Verify no GET call was made (overwrite doesn't check existing fields)
179+ const getCalls = hubspotCallStub . getCalls ( ) . filter ( c => c . args [ 0 ] === 'get' ) ;
180+ assert . strictEqual ( getCalls . length , 0 , 'GET should not be called for overwrite strategy' ) ;
181+
182+ // Verify PATCH includes all provided properties
183+ const patchCall = hubspotCallStub . getCalls ( ) . find ( c => c . args [ 0 ] === 'patch' ) ;
184+ assert ( patchCall , 'PATCH should have been called' ) ;
185+ assert . strictEqual ( patchCall . args [ 2 ] . properties . name , 'Overwritten Name' ) ;
186+ assert . strictEqual ( patchCall . args [ 2 ] . properties . industry , 'Finance' ) ;
187+
188+ // Verify sendJson called with updated: true
189+ assert ( context . sendJson . calledOnce , 'sendJson should be called once' ) ;
190+ const output = context . sendJson . getCall ( 0 ) . args [ 0 ] ;
191+ assert . strictEqual ( output . updated , true ) ;
192+ } ) ;
193+
194+ it ( 'should handle no fields to update with merge strategy' , async ( ) => {
195+ context . messages = {
196+ in : {
197+ content : {
198+ companyId : '123' ,
199+ name : 'Already Set'
200+ }
201+ }
202+ } ;
203+
204+ const existingCompanyData = {
205+ id : '123' ,
206+ properties : {
207+ name : 'Already Set' // non-empty, matches input -- nothing to update
208+ }
209+ } ;
210+
211+ // First GET: merge strategy fetches existing company
212+ // Second GET: "no fields to update" path also fetches existing company
213+ hubspotCallStub . withArgs ( 'get' , 'crm/v3/objects/companies/123' ) . resolves ( {
214+ data : existingCompanyData
215+ } ) ;
216+
217+ await component . receive ( context ) ;
218+
219+ // Verify no PATCH call was made
220+ const patchCalls = hubspotCallStub . getCalls ( ) . filter ( c => c . args [ 0 ] === 'patch' ) ;
221+ assert . strictEqual ( patchCalls . length , 0 , 'PATCH should not be called when no fields to update' ) ;
222+
223+ // Verify sendJson called with updated: false
224+ assert ( context . sendJson . calledOnce , 'sendJson should be called once' ) ;
225+ const output = context . sendJson . getCall ( 0 ) . args [ 0 ] ;
226+ assert . strictEqual ( output . updated , false ) ;
227+ assert . strictEqual ( output . message , 'No empty fields to update (merge strategy)' ) ;
228+ } ) ;
229+
230+ it ( 'should throw error when domain not found' , async ( ) => {
231+ context . messages = {
232+ in : {
233+ content : {
234+ domain : 'nonexistent.com' ,
235+ name : 'Some Company'
236+ }
237+ }
238+ } ;
239+
240+ // Search returns empty results
241+ hubspotCallStub . withArgs ( 'post' , 'crm/v3/objects/companies/search' ) . resolves ( {
242+ data : {
243+ results : [ ]
244+ }
245+ } ) ;
246+
247+ try {
248+ await component . receive ( context ) ;
249+ assert . fail ( 'Should have thrown an error' ) ;
250+ } catch ( error ) {
251+ assert . strictEqual ( error . message , 'Company with domain "nonexistent.com" not found!' ) ;
252+ }
253+ } ) ;
254+
255+ it ( 'should handle additionalProperties for custom fields (e.g. Clearbit fields)' , async ( ) => {
256+ context . messages = {
257+ in : {
258+ content : {
259+ companyId : '123' ,
260+ name : 'Test Corp' ,
261+ updateStrategy : 'overwrite' ,
262+ additionalProperties : {
263+ AND : [
264+ { name : 'clearbit_industry' , value : 'Technology' } ,
265+ { name : 'clearbit_employee_count' , value : '500' } ,
266+ { name : 'pageviews_last_30_days' , value : '42' }
267+ ]
268+ }
269+ }
270+ }
271+ } ;
272+
273+ hubspotCallStub . withArgs ( 'patch' , 'crm/v3/objects/companies/123' ) . resolves ( {
274+ data : {
275+ id : '123' ,
276+ properties : {
277+ name : 'Test Corp' ,
278+ clearbit_industry : 'Technology' ,
279+ clearbit_employee_count : '500' ,
280+ pageviews_last_30_days : '42'
281+ }
282+ }
283+ } ) ;
284+
285+ await component . receive ( context ) ;
286+
287+ // Verify PATCH payload includes both standard and additional properties
288+ const patchCall = hubspotCallStub . getCalls ( ) . find ( c => c . args [ 0 ] === 'patch' ) ;
289+ assert ( patchCall , 'PATCH should have been called' ) ;
290+ const patchedProps = patchCall . args [ 2 ] . properties ;
291+ assert . strictEqual ( patchedProps . name , 'Test Corp' ) ;
292+ assert . strictEqual ( patchedProps . clearbit_industry , 'Technology' ) ;
293+ assert . strictEqual ( patchedProps . clearbit_employee_count , '500' ) ;
294+ assert . strictEqual ( patchedProps . pageviews_last_30_days , '42' ) ;
295+
296+ // Verify sendJson called with updated: true
297+ assert ( context . sendJson . calledOnce , 'sendJson should be called once' ) ;
298+ const output = context . sendJson . getCall ( 0 ) . args [ 0 ] ;
299+ assert . strictEqual ( output . updated , true ) ;
300+ } ) ;
55301} ) ;
0 commit comments