11import * as sp from 'node:stream/promises' ;
22import * as csvp from 'csv-parse' ;
33import { endOfDay , startOfDay } from 'date-fns' ;
4+ import { pollFor } from 'testkit/flow' ;
45import { graphql } from 'testkit/gql' ;
56import * as GraphQLSchema from 'testkit/gql/graphql' ;
67import { execute } from 'testkit/graphql' ;
@@ -112,27 +113,6 @@ test.concurrent('Try to export Audit Logs from an Organization with authorized u
112113 const { createProject, organization } = await createOrg ( ) ;
113114 await createProject ( GraphQLSchema . ProjectType . Single ) ;
114115
115- const exportAuditLogs = await execute ( {
116- document : ExportAllAuditLogs ,
117- variables : {
118- input : {
119- selector : {
120- organizationSlug : organization . slug ,
121- } ,
122- filter : {
123- startDate : lastYear . toISOString ( ) ,
124- endDate : today . toISOString ( ) ,
125- } ,
126- } ,
127- } ,
128- token : ownerToken ,
129- } ) ;
130- expect ( exportAuditLogs . rawBody . data ?. exportOrganizationAuditLog . error ) . toBeNull ( ) ;
131- const url = exportAuditLogs . rawBody . data ?. exportOrganizationAuditLog . ok ?. url ;
132- const bodyStream = await fetchAuditLogFromS3Bucket ( String ( url ) ) ;
133- const rows = bodyStream . split ( '\n' ) ;
134- expect ( rows . length ) . toBeGreaterThan ( 1 ) ; // At least header and one row
135- const header = rows ?. [ 0 ] . split ( ',' ) ;
136116 const expectedHeader = [
137117 'id' ,
138118 'created_at' ,
@@ -142,11 +122,50 @@ test.concurrent('Try to export Audit Logs from an Organization with authorized u
142122 'access_token_id' ,
143123 'metadata' ,
144124 ] ;
145- expect ( header ) . toEqual ( expectedHeader ) ;
146- // Sometimes the order of the rows is not guaranteed, so we need to check if the expected rows are present
147- expect ( rows ?. find ( row => row . includes ( 'ORGANIZATION_CREATED' ) ) ) . toBeDefined ( ) ;
148- expect ( rows ?. find ( row => row . includes ( 'PROJECT_CREATED' ) ) ) . toBeDefined ( ) ;
149- expect ( rows ?. find ( row => row . includes ( 'TARGET_CREATED' ) ) ) . toBeDefined ( ) ;
125+
126+ // Poll until all expected audit log entries are visible in ClickHouse
127+ // ClickHouse has eventual consistency, so data may not be immediately visible after INSERT
128+ await pollFor ( async ( ) => {
129+ const exportAuditLogs = await execute ( {
130+ document : ExportAllAuditLogs ,
131+ variables : {
132+ input : {
133+ selector : {
134+ organizationSlug : organization . slug ,
135+ } ,
136+ filter : {
137+ startDate : lastYear . toISOString ( ) ,
138+ endDate : today . toISOString ( ) ,
139+ } ,
140+ } ,
141+ } ,
142+ token : ownerToken ,
143+ } ) ;
144+
145+ if ( exportAuditLogs . rawBody . data ?. exportOrganizationAuditLog . error ) {
146+ return false ;
147+ }
148+
149+ const url = exportAuditLogs . rawBody . data ?. exportOrganizationAuditLog . ok ?. url ;
150+ const bodyStream = await fetchAuditLogFromS3Bucket ( String ( url ) ) ;
151+ const rows = bodyStream . split ( '\n' ) ;
152+
153+ if ( rows . length <= 1 ) {
154+ return false ;
155+ }
156+
157+ const header = rows [ 0 ] . split ( ',' ) ;
158+ if ( JSON . stringify ( header ) !== JSON . stringify ( expectedHeader ) ) {
159+ return false ;
160+ }
161+
162+ // Check if all expected audit log entries are present
163+ const hasOrgCreated = rows . some ( row => row . includes ( 'ORGANIZATION_CREATED' ) ) ;
164+ const hasProjectCreated = rows . some ( row => row . includes ( 'PROJECT_CREATED' ) ) ;
165+ const hasTargetCreated = rows . some ( row => row . includes ( 'TARGET_CREATED' ) ) ;
166+
167+ return hasOrgCreated && hasProjectCreated && hasTargetCreated ;
168+ } ) ;
150169} ) ;
151170
152171test . concurrent ( 'export audit log for schema policy' , async ( ) => {
0 commit comments