11/**
22 * @vitest -environment node
33 */
4- import { auditMock , databaseMock , loggerMock } from '@sim/testing'
4+ import { auditMock , databaseMock , drizzleOrmMock , loggerMock } from '@sim/testing'
55import { afterEach , beforeEach , describe , expect , it , vi } from 'vitest'
66
77vi . mock ( '@sim/db' , ( ) => ( {
88 ...databaseMock ,
99 auditLog : { id : 'id' , workspaceId : 'workspace_id' } ,
1010} ) )
11+ vi . mock ( '@sim/db/schema' , ( ) => ( {
12+ user : { id : 'id' , name : 'name' , email : 'email' } ,
13+ } ) )
14+ vi . mock ( 'drizzle-orm' , ( ) => drizzleOrmMock )
1115vi . mock ( '@sim/logger' , ( ) => loggerMock )
1216vi . mock ( 'nanoid' , ( ) => ( { nanoid : ( ) => 'test-id-123' } ) )
1317
1418import { AuditAction , AuditResourceType , recordAudit } from '@/lib/audit/log'
1519
20+ const flush = ( ) => new Promise ( ( resolve ) => setTimeout ( resolve , 10 ) )
21+
1622describe ( 'AuditAction' , ( ) => {
1723 it ( 'contains all expected action categories' , ( ) => {
1824 expect ( AuditAction . WORKFLOW_CREATED ) . toBe ( 'workflow.created' )
@@ -45,12 +51,22 @@ describe('AuditResourceType', () => {
4551
4652describe ( 'recordAudit' , ( ) => {
4753 const mockInsert = databaseMock . db . insert
54+ const mockSelect = databaseMock . db . select
4855 let mockValues : ReturnType < typeof vi . fn >
56+ let mockLimit : ReturnType < typeof vi . fn >
4957
5058 beforeEach ( ( ) => {
5159 vi . clearAllMocks ( )
5260 mockValues = vi . fn ( ( ) => Promise . resolve ( ) )
5361 mockInsert . mockReturnValue ( { values : mockValues } )
62+ mockLimit = vi . fn ( ( ) => Promise . resolve ( [ ] ) )
63+ mockSelect . mockReturnValue ( {
64+ from : vi . fn ( ( ) => ( {
65+ where : vi . fn ( ( ) => ( {
66+ limit : mockLimit ,
67+ } ) ) ,
68+ } ) ) ,
69+ } )
5470 } )
5571
5672 afterEach ( ( ) => {
@@ -61,15 +77,16 @@ describe('recordAudit', () => {
6177 recordAudit ( {
6278 workspaceId : 'ws-1' ,
6379 actorId : 'user-1' ,
80+ actorName : 'Test User' ,
81+ actorEmail : 'test@example.com' ,
6482 action : AuditAction . WORKFLOW_CREATED ,
6583 resourceType : AuditResourceType . WORKFLOW ,
6684 resourceId : 'wf-1' ,
6785 } )
6886
69- await vi . waitFor ( ( ) => {
70- expect ( mockInsert ) . toHaveBeenCalledTimes ( 1 )
71- } )
87+ await flush ( )
7288
89+ expect ( mockInsert ) . toHaveBeenCalledTimes ( 1 )
7390 expect ( mockValues ) . toHaveBeenCalledWith (
7491 expect . objectContaining ( {
7592 id : 'test-id-123' ,
@@ -96,9 +113,7 @@ describe('recordAudit', () => {
96113 description : 'Created folder "My Folder"' ,
97114 } )
98115
99- await vi . waitFor ( ( ) => {
100- expect ( mockValues ) . toHaveBeenCalledTimes ( 1 )
101- } )
116+ await flush ( )
102117
103118 expect ( mockValues ) . toHaveBeenCalledWith (
104119 expect . objectContaining ( {
@@ -110,26 +125,6 @@ describe('recordAudit', () => {
110125 )
111126 } )
112127
113- it ( 'sets optional fields to undefined when not provided' , async ( ) => {
114- recordAudit ( {
115- workspaceId : 'ws-1' ,
116- actorId : 'user-1' ,
117- action : AuditAction . WORKSPACE_DELETED ,
118- resourceType : AuditResourceType . WORKSPACE ,
119- } )
120-
121- await vi . waitFor ( ( ) => {
122- expect ( mockValues ) . toHaveBeenCalledTimes ( 1 )
123- } )
124-
125- const insertedValues = mockValues . mock . calls [ 0 ] [ 0 ]
126- expect ( insertedValues . resourceId ) . toBeUndefined ( )
127- expect ( insertedValues . actorName ) . toBeUndefined ( )
128- expect ( insertedValues . actorEmail ) . toBeUndefined ( )
129- expect ( insertedValues . resourceName ) . toBeUndefined ( )
130- expect ( insertedValues . description ) . toBeUndefined ( )
131- } )
132-
133128 it ( 'extracts IP address from x-forwarded-for header' , async ( ) => {
134129 const request = new Request ( 'https://example.com' , {
135130 headers : {
@@ -141,14 +136,14 @@ describe('recordAudit', () => {
141136 recordAudit ( {
142137 workspaceId : 'ws-1' ,
143138 actorId : 'user-1' ,
139+ actorName : 'Test' ,
140+ actorEmail : 'test@test.com' ,
144141 action : AuditAction . MEMBER_INVITED ,
145142 resourceType : AuditResourceType . WORKSPACE ,
146143 request,
147144 } )
148145
149- await vi . waitFor ( ( ) => {
150- expect ( mockValues ) . toHaveBeenCalledTimes ( 1 )
151- } )
146+ await flush ( )
152147
153148 expect ( mockValues ) . toHaveBeenCalledWith (
154149 expect . objectContaining ( {
@@ -166,14 +161,14 @@ describe('recordAudit', () => {
166161 recordAudit ( {
167162 workspaceId : 'ws-1' ,
168163 actorId : 'user-1' ,
164+ actorName : 'Test' ,
165+ actorEmail : 'test@test.com' ,
169166 action : AuditAction . API_KEY_CREATED ,
170167 resourceType : AuditResourceType . API_KEY ,
171168 request,
172169 } )
173170
174- await vi . waitFor ( ( ) => {
175- expect ( mockValues ) . toHaveBeenCalledTimes ( 1 )
176- } )
171+ await flush ( )
177172
178173 expect ( mockValues ) . toHaveBeenCalledWith (
179174 expect . objectContaining ( {
@@ -187,13 +182,13 @@ describe('recordAudit', () => {
187182 recordAudit ( {
188183 workspaceId : 'ws-1' ,
189184 actorId : 'user-1' ,
185+ actorName : 'Test' ,
186+ actorEmail : 'test@test.com' ,
190187 action : AuditAction . ENVIRONMENT_UPDATED ,
191188 resourceType : AuditResourceType . ENVIRONMENT ,
192189 } )
193190
194- await vi . waitFor ( ( ) => {
195- expect ( mockValues ) . toHaveBeenCalledTimes ( 1 )
196- } )
191+ await flush ( )
197192
198193 expect ( mockValues ) . toHaveBeenCalledWith ( expect . objectContaining ( { metadata : { } } ) )
199194 } )
@@ -202,14 +197,14 @@ describe('recordAudit', () => {
202197 recordAudit ( {
203198 workspaceId : 'ws-1' ,
204199 actorId : 'user-1' ,
200+ actorName : 'Test' ,
201+ actorEmail : 'test@test.com' ,
205202 action : AuditAction . WEBHOOK_CREATED ,
206203 resourceType : AuditResourceType . WEBHOOK ,
207204 metadata : { provider : 'github' , workflowId : 'wf-1' } ,
208205 } )
209206
210- await vi . waitFor ( ( ) => {
211- expect ( mockValues ) . toHaveBeenCalledTimes ( 1 )
212- } )
207+ await flush ( )
213208
214209 expect ( mockValues ) . toHaveBeenCalledWith (
215210 expect . objectContaining ( {
@@ -219,28 +214,138 @@ describe('recordAudit', () => {
219214 } )
220215
221216 it ( 'does not throw when the database insert fails' , async ( ) => {
222- mockValues . mockReturnValue ( Promise . reject ( new Error ( 'DB connection lost' ) ) )
217+ mockValues . mockImplementation ( ( ) => Promise . reject ( new Error ( 'DB connection lost' ) ) )
223218
224219 expect ( ( ) => {
225220 recordAudit ( {
226221 workspaceId : 'ws-1' ,
227222 actorId : 'user-1' ,
223+ actorName : 'Test' ,
224+ actorEmail : 'test@test.com' ,
228225 action : AuditAction . WORKFLOW_DELETED ,
229226 resourceType : AuditResourceType . WORKFLOW ,
230227 } )
231228 } ) . not . toThrow ( )
229+
230+ await flush ( )
232231 } )
233232
234233 it ( 'does not block — returns void synchronously' , ( ) => {
235234 const result = recordAudit ( {
236235 workspaceId : 'ws-1' ,
237236 actorId : 'user-1' ,
237+ actorName : 'Test' ,
238+ actorEmail : 'test@test.com' ,
238239 action : AuditAction . CHAT_DEPLOYED ,
239240 resourceType : AuditResourceType . CHAT ,
240241 } )
241242
242243 expect ( result ) . toBeUndefined ( )
243244 } )
245+
246+ describe ( 'lazy actor resolution' , ( ) => {
247+ it ( 'looks up user when actorName and actorEmail are both undefined' , async ( ) => {
248+ mockLimit . mockResolvedValue ( [ { name : 'Resolved Name' , email : 'resolved@example.com' } ] )
249+
250+ recordAudit ( {
251+ workspaceId : 'ws-1' ,
252+ actorId : 'user-1' ,
253+ action : AuditAction . DOCUMENT_UPLOADED ,
254+ resourceType : AuditResourceType . DOCUMENT ,
255+ resourceId : 'doc-1' ,
256+ } )
257+
258+ await flush ( )
259+
260+ expect ( mockSelect ) . toHaveBeenCalledTimes ( 1 )
261+ expect ( mockValues ) . toHaveBeenCalledWith (
262+ expect . objectContaining ( {
263+ actorName : 'Resolved Name' ,
264+ actorEmail : 'resolved@example.com' ,
265+ } )
266+ )
267+ } )
268+
269+ it ( 'skips lookup when actorName is provided (even if null)' , async ( ) => {
270+ recordAudit ( {
271+ workspaceId : 'ws-1' ,
272+ actorId : 'user-1' ,
273+ actorName : null ,
274+ actorEmail : null ,
275+ action : AuditAction . DOCUMENT_UPLOADED ,
276+ resourceType : AuditResourceType . DOCUMENT ,
277+ } )
278+
279+ await flush ( )
280+
281+ expect ( mockSelect ) . not . toHaveBeenCalled ( )
282+ } )
283+
284+ it ( 'skips lookup when actorName and actorEmail are provided' , async ( ) => {
285+ recordAudit ( {
286+ workspaceId : 'ws-1' ,
287+ actorId : 'user-1' ,
288+ actorName : 'Already Known' ,
289+ actorEmail : 'known@example.com' ,
290+ action : AuditAction . WORKFLOW_CREATED ,
291+ resourceType : AuditResourceType . WORKFLOW ,
292+ } )
293+
294+ await flush ( )
295+
296+ expect ( mockSelect ) . not . toHaveBeenCalled ( )
297+ expect ( mockValues ) . toHaveBeenCalledWith (
298+ expect . objectContaining ( {
299+ actorName : 'Already Known' ,
300+ actorEmail : 'known@example.com' ,
301+ } )
302+ )
303+ } )
304+
305+ it ( 'inserts without actor info when lookup fails' , async ( ) => {
306+ mockLimit . mockRejectedValue ( new Error ( 'DB down' ) )
307+
308+ recordAudit ( {
309+ workspaceId : 'ws-1' ,
310+ actorId : 'user-1' ,
311+ action : AuditAction . KNOWLEDGE_BASE_CREATED ,
312+ resourceType : AuditResourceType . KNOWLEDGE_BASE ,
313+ } )
314+
315+ await flush ( )
316+
317+ expect ( mockSelect ) . toHaveBeenCalledTimes ( 1 )
318+ expect ( mockValues ) . toHaveBeenCalledWith (
319+ expect . objectContaining ( {
320+ actorId : 'user-1' ,
321+ actorName : undefined ,
322+ actorEmail : undefined ,
323+ } )
324+ )
325+ } )
326+
327+ it ( 'sets actor info to null when user is not found' , async ( ) => {
328+ mockLimit . mockResolvedValue ( [ ] )
329+
330+ recordAudit ( {
331+ workspaceId : 'ws-1' ,
332+ actorId : 'deleted-user' ,
333+ action : AuditAction . WORKFLOW_DELETED ,
334+ resourceType : AuditResourceType . WORKFLOW ,
335+ } )
336+
337+ await flush ( )
338+
339+ expect ( mockSelect ) . toHaveBeenCalledTimes ( 1 )
340+ expect ( mockValues ) . toHaveBeenCalledWith (
341+ expect . objectContaining ( {
342+ actorId : 'deleted-user' ,
343+ actorName : undefined ,
344+ actorEmail : undefined ,
345+ } )
346+ )
347+ } )
348+ } )
244349} )
245350
246351describe ( 'auditMock sync' , ( ) => {
0 commit comments