@@ -276,6 +276,16 @@ describe('serverAccessLogger utility functions', () => {
276276 // Should return BACKBEAT instead of UNKNOWN
277277 assert . strictEqual ( result , 'REST.GET.BACKBEAT' ) ;
278278 } ) ;
279+
280+ it ( 'should return S3.EXPIRE.OBJECT for lifecycle expiration requests' , ( ) => {
281+ const req = {
282+ method : 'DELETE' ,
283+ apiMethod : 'objectDelete' ,
284+ serverAccessLog : { backbeat : true , expiration : true } ,
285+ } ;
286+ const result = getOperation ( req ) ;
287+ assert . strictEqual ( result , 'S3.EXPIRE.OBJECT' ) ;
288+ } ) ;
279289 } ) ;
280290
281291 describe ( 'getRequester' , ( ) => {
@@ -1401,6 +1411,224 @@ describe('serverAccessLogger utility functions', () => {
14011411 assert . strictEqual ( loggedData . loggingTargetBucket , 'log-bucket' ) ;
14021412 assert . strictEqual ( loggedData . loggingTargetPrefix , 'logs/' ) ;
14031413 } ) ;
1414+
1415+ it ( 'should pass through loggingEnabled for lifecycle expiration' , ( ) => {
1416+ setServerAccessLogger ( mockLogger ) ;
1417+ const req = {
1418+ serverAccessLog : {
1419+ backbeat : true ,
1420+ expiration : true ,
1421+ enabled : true ,
1422+ loggingEnabled : {
1423+ TargetBucket : 'log-bucket' ,
1424+ TargetPrefix : 'logs/' ,
1425+ } ,
1426+ } ,
1427+ method : 'DELETE' ,
1428+ apiMethod : 'objectDelete' ,
1429+ headers : { } ,
1430+ socket : { } ,
1431+ } ;
1432+ const res = {
1433+ serverAccessLog : { } ,
1434+ getHeader : ( ) => null ,
1435+ } ;
1436+
1437+ logServerAccess ( req , res ) ;
1438+
1439+ assert . strictEqual ( mockLogger . write . callCount , 1 ) ;
1440+ const loggedData = JSON . parse ( mockLogger . write . firstCall . args [ 0 ] . trim ( ) ) ;
1441+ assert . strictEqual ( loggedData . loggingEnabled , true ) ;
1442+ assert . strictEqual ( loggedData . loggingTargetBucket , 'log-bucket' ) ;
1443+ assert . strictEqual ( loggedData . loggingTargetPrefix , 'logs/' ) ;
1444+ assert . strictEqual ( loggedData . operation , 'S3.EXPIRE.OBJECT' ) ;
1445+ } ) ;
1446+
1447+ it ( 'should force loggingEnabled false for non-expiration backbeat' , ( ) => {
1448+ setServerAccessLogger ( mockLogger ) ;
1449+ const req = {
1450+ serverAccessLog : {
1451+ backbeat : true ,
1452+ enabled : true ,
1453+ loggingEnabled : {
1454+ TargetBucket : 'log-bucket' ,
1455+ TargetPrefix : 'logs/' ,
1456+ } ,
1457+ } ,
1458+ method : 'PUT' ,
1459+ apiMethod : 'objectPut' ,
1460+ headers : { } ,
1461+ socket : { } ,
1462+ } ;
1463+ const res = {
1464+ serverAccessLog : { } ,
1465+ getHeader : ( ) => null ,
1466+ } ;
1467+
1468+ logServerAccess ( req , res ) ;
1469+
1470+ assert . strictEqual ( mockLogger . write . callCount , 1 ) ;
1471+ const loggedData = JSON . parse ( mockLogger . write . firstCall . args [ 0 ] . trim ( ) ) ;
1472+ assert . strictEqual ( loggedData . loggingEnabled , false ) ;
1473+ } ) ;
1474+
1475+ it ( 'should not deliver lifecycle expiration log when bucket has no logging config' , ( ) => {
1476+ setServerAccessLogger ( mockLogger ) ;
1477+ const req = {
1478+ serverAccessLog : {
1479+ backbeat : true ,
1480+ expiration : true ,
1481+ enabled : false ,
1482+ } ,
1483+ method : 'DELETE' ,
1484+ apiMethod : 'objectDelete' ,
1485+ headers : { } ,
1486+ socket : { } ,
1487+ } ;
1488+ const res = {
1489+ serverAccessLog : { } ,
1490+ getHeader : ( ) => null ,
1491+ } ;
1492+
1493+ logServerAccess ( req , res ) ;
1494+
1495+ assert . strictEqual ( mockLogger . write . callCount , 1 ) ;
1496+ const loggedData = JSON . parse ( mockLogger . write . firstCall . args [ 0 ] . trim ( ) ) ;
1497+ assert . strictEqual ( loggedData . loggingEnabled , false ) ;
1498+ } ) ;
1499+
1500+ it ( 'should produce a complete log entry for lifecycle expiration' , ( ) => {
1501+ setServerAccessLogger ( mockLogger ) ;
1502+ const authInfo = {
1503+ getAccountDisplayName : ( ) => 'lifecycleAccount' ,
1504+ getCanonicalID : ( ) => 'lifecycle-canonical-id' ,
1505+ isRequesterPublicUser : ( ) => false ,
1506+ isRequesterAnIAMUser : ( ) => false ,
1507+ getAuthVersion : ( ) => 'AWS4-HMAC-SHA256' ,
1508+ getAuthType : ( ) => 'REST-HEADER' ,
1509+ getAccessKey : ( ) => 'lifecycle-access-key' ,
1510+ } ;
1511+ const startTime = process . hrtime . bigint ( ) ;
1512+ const startTurnAroundTime = startTime + 1_000_000n ;
1513+ const endTurnAroundTime = startTurnAroundTime + 50_000_000n ;
1514+ const onFinishEndTime = startTime + 100_000_000n ;
1515+ const onCloseEndTime = startTime + 110_000_000n ;
1516+
1517+ const req = {
1518+ serverAccessLog : {
1519+ backbeat : true ,
1520+ expiration : true ,
1521+ enabled : true ,
1522+ loggingEnabled : {
1523+ TargetBucket : 'log-bucket' ,
1524+ TargetPrefix : 'access-logs/' ,
1525+ } ,
1526+ bucketOwner : 'bucket-owner-id' ,
1527+ bucketName : 'source-bucket' ,
1528+ objectKey : 'expired-object.txt' ,
1529+ authInfo,
1530+ analyticsAction : 'deleteObjectFromExpiration' ,
1531+ analyticsAccountName : 'lifecycleAccount' ,
1532+ analyticsUserName : '' ,
1533+ startTime,
1534+ startTimeUnixMS : Date . now ( ) ,
1535+ startTurnAroundTime,
1536+ onFinishEndTime,
1537+ onCloseEndTime,
1538+ objectSize : 1024 ,
1539+ analyticsBytesDeleted : 1024 ,
1540+ } ,
1541+ method : 'DELETE' ,
1542+ apiMethod : 'objectDelete' ,
1543+ headers : { host : 'source-bucket.s3.amazonaws.com' } ,
1544+ socket : { } ,
1545+ } ;
1546+ const res = {
1547+ serverAccessLog : {
1548+ endTurnAroundTime,
1549+ } ,
1550+ statusCode : 200 ,
1551+ getHeader : ( ) => null ,
1552+ } ;
1553+
1554+ logServerAccess ( req , res ) ;
1555+
1556+ assert . strictEqual ( mockLogger . write . callCount , 1 ) ;
1557+ const loggedData = JSON . parse ( mockLogger . write . firstCall . args [ 0 ] . trim ( ) ) ;
1558+
1559+ // Core expiration fields
1560+ assert . strictEqual ( loggedData . operation , 'S3.EXPIRE.OBJECT' ) ;
1561+ assert . strictEqual ( loggedData . loggingEnabled , true ) ;
1562+ assert . strictEqual ( loggedData . loggingTargetBucket , 'log-bucket' ) ;
1563+ assert . strictEqual ( loggedData . loggingTargetPrefix , 'access-logs/' ) ;
1564+
1565+ // Bucket and object info
1566+ assert . strictEqual ( loggedData . bucketOwner , 'bucket-owner-id' ) ;
1567+ assert . strictEqual ( loggedData . bucketName , 'source-bucket' ) ;
1568+ assert . strictEqual ( loggedData . objectKey , 'expired-object.txt' ) ;
1569+
1570+ // Requester is the lifecycle service, not the auth identity
1571+ assert . strictEqual ( loggedData . requester , 'ScalityS3LifecycleService' ) ;
1572+
1573+ // Object size
1574+ assert . strictEqual ( loggedData . objectSize , 1024 ) ;
1575+ assert . strictEqual ( loggedData . bytesDeleted , 1024 ) ;
1576+
1577+ // HTTP-layer fields are null (internal operation, not an HTTP request)
1578+ assert . strictEqual ( loggedData . clientIP , undefined ) ;
1579+ assert . strictEqual ( loggedData . requestURI , undefined ) ;
1580+ assert . strictEqual ( loggedData . httpCode , undefined ) ;
1581+ assert . strictEqual ( loggedData . bytesSent , undefined ) ;
1582+ assert . strictEqual ( loggedData . totalTime , undefined ) ;
1583+ assert . strictEqual ( loggedData . turnAroundTime , undefined ) ;
1584+ assert . strictEqual ( loggedData . signatureVersion , undefined ) ;
1585+ assert . strictEqual ( loggedData . authenticationType , undefined ) ;
1586+ assert . strictEqual ( loggedData . hostHeader , undefined ) ;
1587+ assert . strictEqual ( loggedData . awsAccessKeyID , undefined ) ;
1588+ } ) ;
1589+
1590+ it ( 'should log lifecycle expiration with error code on failure' , ( ) => {
1591+ setServerAccessLogger ( mockLogger ) ;
1592+ const req = {
1593+ serverAccessLog : {
1594+ backbeat : true ,
1595+ expiration : true ,
1596+ enabled : true ,
1597+ loggingEnabled : {
1598+ TargetBucket : 'log-bucket' ,
1599+ TargetPrefix : 'logs/' ,
1600+ } ,
1601+ bucketOwner : 'bucket-owner-id' ,
1602+ bucketName : 'source-bucket' ,
1603+ objectKey : 'expired-object.txt' ,
1604+ startTime : process . hrtime . bigint ( ) ,
1605+ startTimeUnixMS : Date . now ( ) ,
1606+ } ,
1607+ method : 'DELETE' ,
1608+ apiMethod : 'objectDelete' ,
1609+ headers : { } ,
1610+ socket : { } ,
1611+ } ;
1612+ const res = {
1613+ serverAccessLog : {
1614+ errorCode : 'NoSuchKey' ,
1615+ endTurnAroundTime : process . hrtime . bigint ( ) ,
1616+ } ,
1617+ statusCode : 404 ,
1618+ getHeader : ( ) => null ,
1619+ } ;
1620+
1621+ logServerAccess ( req , res ) ;
1622+
1623+ assert . strictEqual ( mockLogger . write . callCount , 1 ) ;
1624+ const loggedData = JSON . parse ( mockLogger . write . firstCall . args [ 0 ] . trim ( ) ) ;
1625+ assert . strictEqual ( loggedData . operation , 'S3.EXPIRE.OBJECT' ) ;
1626+ assert . strictEqual ( loggedData . loggingEnabled , true ) ;
1627+ assert . strictEqual ( loggedData . requester , 'ScalityS3LifecycleService' ) ;
1628+ assert . strictEqual ( loggedData . errorCode , 'NoSuchKey' ) ;
1629+ assert . strictEqual ( loggedData . httpCode , undefined ) ;
1630+ } ) ;
1631+
14041632 } ) ;
14051633} ) ;
14061634
0 commit comments