diff --git a/lib/models/LifecycleConfiguration.ts b/lib/models/LifecycleConfiguration.ts index a9ce1c729..f6f480b15 100644 --- a/lib/models/LifecycleConfiguration.ts +++ b/lib/models/LifecycleConfiguration.ts @@ -6,6 +6,7 @@ import type { ArsenalError } from '../errors'; import LifecycleRule from './LifecycleRule'; import escapeForXml from '../s3middleware/escapeForXml'; import { Status } from './LifecycleRule'; +import type { RequestLogger } from 'werelogs'; const MAX_DAYS = 2147483647; // Max 32-bit signed binary integer. @@ -17,7 +18,7 @@ export const ValidLifecycleRules = [ 'Transition', 'NoncurrentVersionTransition', ] as const; -type LifecycleAction = typeof ValidLifecycleRules[number]; +type LifecycleAction = (typeof ValidLifecycleRules)[number]; /** * Format of xml request: @@ -95,7 +96,7 @@ type LifecycleAction = typeof ValidLifecycleRules[number]; }; */ export type LifecycleConfigurationMetadata = { - rules: Rule[], + rules: Rule[]; }; // This matches the XML structure of the rule, as generated by xml2js: so every field is actually @@ -122,22 +123,26 @@ export default class LifecycleConfiguration { error?: ArsenalError; rules?: Rule[]; }; + _log: RequestLogger; /** * Create a Lifecycle Configuration instance * @param xml - the parsed xml + * @param log - werelogs logger * @param config - the CloudServer config * @return - LifecycleConfiguration instance */ constructor( xml: any, + log: RequestLogger, config: { - replicationEndpoints: { site: string }[], - locationConstraints?: Record, - supportedLifecycleRules: string[], - } + replicationEndpoints: { site: string }[]; + locationConstraints?: Record; + supportedLifecycleRules: string[]; + }, ) { this._parsedXML = xml; + this._log = log; this._storageClasses = this._buildStorageClasses(config); this._supportedLifecycleRules = config.supportedLifecycleRules; this._ruleIDs = []; @@ -153,8 +158,8 @@ export default class LifecycleConfiguration { * @returns An array of valid storage class names. */ _buildStorageClasses(config: { - replicationEndpoints: { site: string }[], - locationConstraints?: Record, + replicationEndpoints: { site: string }[]; + locationConstraints?: Record; }) { return config.replicationEndpoints .map(endpoint => endpoint.site) @@ -185,8 +190,7 @@ export default class LifecycleConfiguration { const error = errorInstances.MalformedXML.customizeDescription(msg); return { error }; } - if (!this._parsedXML.LifecycleConfiguration && - this._parsedXML.LifecycleConfiguration !== '') { + if (!this._parsedXML.LifecycleConfiguration && this._parsedXML.LifecycleConfiguration !== '') { const msg = 'request xml does not include LifecycleConfiguration'; const error = errorInstances.MalformedXML.customizeDescription(msg); return { error }; @@ -194,7 +198,7 @@ export default class LifecycleConfiguration { const lifecycleConf = this._parsedXML.LifecycleConfiguration; const rulesArray: LifecycleXMLRule[] = lifecycleConf.Rule; if (!rulesArray || !Array.isArray(rulesArray) || rulesArray.length === 0) { - const msg = 'missing required key \'Rules\' in LifecycleConfiguration'; + const msg = "missing required key 'Rules' in LifecycleConfiguration"; const error = errorInstances.MissingRequiredParameter.customizeDescription(msg); return { error }; } @@ -277,7 +281,7 @@ export default class LifecycleConfiguration { */ _parseRule(rule: LifecycleXMLRule) { // Either Prefix or Filter must be included, but can be empty string - if ((!rule.Filter && rule.Filter !== '') && (!rule.Prefix && rule.Prefix !== '')) { + if (!rule.Filter && rule.Filter !== '' && !rule.Prefix && rule.Prefix !== '') { const msg = 'Rule xml does not include valid Filter or Prefix'; const error = errorInstances.MalformedXML.customizeDescription(msg); return { error }; @@ -352,7 +356,7 @@ export default class LifecycleConfiguration { error?: ArsenalError; propName: 'filter'; rulePrefix: string; - tags: { key: string; val: string }[] + tags: { key: string; val: string }[]; } = { propName: 'filter', }; @@ -366,8 +370,7 @@ export default class LifecycleConfiguration { } return filterObj; } - if (filter.And && (filter.Prefix || filter.Tag) || - (filter.Prefix && filter.Tag)) { + if ((filter.And && (filter.Prefix || filter.Tag)) || (filter.Prefix && filter.Tag)) { const msg = 'Filter should only include one of And, Prefix, or Tag key'; const error = errorInstances.MalformedXML.customizeDescription(msg); filterObj.error = error; @@ -394,7 +397,8 @@ export default class LifecycleConfiguration { const andF = filter.And[0]; if (!andF.Tag || (!andF.Prefix && andF.Tag.length < 2)) { filterObj.error = errorInstances.MalformedXML.customizeDescription( - 'And should include Prefix and Tags or more than one Tag'); + 'And should include Prefix and Tags or more than one Tag', + ); return filterObj; } if (andF.Prefix && andF.Prefix.length >= 1) { @@ -453,20 +457,18 @@ export default class LifecycleConfiguration { if (tag.Key[0].length < 1 || tag.Key[0].length > 128) { tagObj.error = errorInstances.InvalidRequest.customizeDescription( - "A Tag's Key must be a length between 1 and 128" + "A Tag's Key must be a length between 1 and 128", ); break; } if (tag.Value[0].length < 0 || tag.Value[0].length > 256) { tagObj.error = errorInstances.InvalidRequest.customizeDescription( - "A Tag's Value must be a length between 0 and 256" + "A Tag's Value must be a length between 0 and 256", ); break; } if (this._tagKeys.includes(tag.Key[0])) { - tagObj.error = errorInstances.InvalidRequest.customizeDescription( - 'Tag Keys must be unique' - ); + tagObj.error = errorInstances.InvalidRequest.customizeDescription('Tag Keys must be unique'); break; } this._tagKeys.push(tag.Key[0]); @@ -493,9 +495,9 @@ export default class LifecycleConfiguration { */ _parseID(id: string[]) { // @ts-ignore - const idObj: - | { error: ArsenalError; propName: 'ruleID', ruleID?: any } - | { propName: 'ruleID', ruleID: any } = { propName: 'ruleID' }; + const idObj: { error: ArsenalError; propName: 'ruleID'; ruleID?: any } | { propName: 'ruleID'; ruleID: any } = { + propName: 'ruleID', + }; if (id && id[0].length > 255) { const msg = 'Rule ID is greater than 255 characters long'; const error = errorInstances.InvalidArgument.customizeDescription(msg); @@ -587,13 +589,34 @@ export default class LifecycleConfiguration { */ _checkDays(params: { days: number; field: string; ancestor: string }) { const { days, field, ancestor } = params; + if (Number.isNaN(days)) { + return errorInstances.MalformedXML.customizeDescription( + `'${field}' in ${ancestor} action must be an integer`, + ); + } if (days < 0) { - const msg = `'${field}' in ${ancestor} action must be nonnegative`; + // Match AWS wording: expiration and abort actions report "must be a + // positive integer", transitions report "must be nonnegative". + const positiveIntegerActions = [ + 'Expiration', + 'NoncurrentVersionExpiration', + 'AbortIncompleteMultipartUpload', + ]; + const msg = positiveIntegerActions.includes(ancestor) + ? `'${field}' for ${ancestor} action must be a positive integer` + : `'${field}' in ${ancestor} action must be nonnegative`; return errorInstances.InvalidArgument.customizeDescription(msg); } if (days > MAX_DAYS) { return errorInstances.MalformedXML.customizeDescription( - `'${field}' in ${ancestor} action must not exceed ${MAX_DAYS}`); + `'${field}' in ${ancestor} action must not exceed ${MAX_DAYS}`, + ); + } + if (days === 0 && (ancestor === 'Expiration' || ancestor === 'NoncurrentVersionExpiration')) { + this._log.warn('lifecycle rule configured with a 0-day action time', { + field, + ancestor, + }); } return null; } @@ -624,7 +647,8 @@ export default class LifecycleConfiguration { return errorInstances.MalformedXML.customizeDescription(msg); } if (usedStorageClasses.includes(storageClass)) { - const msg = `'StorageClass' must be different for '${ancestor}' ` + + const msg = + `'StorageClass' must be different for '${ancestor}' ` + `actions in same 'Rule' with ${this._getRuleFilterDesc(rule)}`; return errorInstances.InvalidRequest.customizeDescription(msg); } @@ -666,7 +690,8 @@ export default class LifecycleConfiguration { const timeType = days !== undefined ? 'Days' : 'Date'; const filterMsg = this._getRuleFilterDesc(rule); const compareStorageClass = invalidTransition.StorageClass[0]; - const msg = `'${timeType}' in the 'Transition' action for ` + + const msg = + `'${timeType}' in the 'Transition' action for ` + `StorageClass '${storageClass}' for ${filterMsg} must be at ` + `least one day apart from ${filterMsg} in the 'Transition' ` + `action for StorageClass '${compareStorageClass}'`; @@ -693,15 +718,16 @@ export default class LifecycleConfiguration { }) { const { usedTimeType, currentTimeType, rule } = params; if (usedTimeType && usedTimeType !== currentTimeType) { - const msg = "Found mixed 'Date' and 'Days' based Transition " + + const msg = + "Found mixed 'Date' and 'Days' based Transition " + 'actions in lifecycle rule for ' + `${this._getRuleFilterDesc(rule)}`; return errorInstances.InvalidRequest.customizeDescription(msg); } // Transition time type cannot differ from the expiration, if provided. - if (rule.Expiration && - rule.Expiration[0][currentTimeType] === undefined) { - const msg = "Found mixed 'Date' and 'Days' based Expiration and " + + if (rule.Expiration && rule.Expiration[0][currentTimeType] === undefined) { + const msg = + "Found mixed 'Date' and 'Days' based Expiration and " + 'Transition actions in lifecycle rule for ' + `${this._getRuleFilterDesc(rule)}`; return errorInstances.InvalidRequest.customizeDescription(msg); @@ -717,13 +743,13 @@ export default class LifecycleConfiguration { _checkDate(date: string) { const isoRegex = new RegExp( '^(-?(?:[1-9][0-9]*)?[0-9]{4})' + // Year - '-(1[0-2]|0[1-9])' + // Month - '-(3[01]|0[1-9]|[12][0-9])' + // Day - 'T(2[0-3]|[01][0-9])' + // Hour - ':([0-5][0-9])' + // Minute - ':([0-5][0-9])' + // Second - '(\\.[0-9]+)?' + // Fractional second - '(Z|[+-][01][0-9]:[0-5][0-9])?$', // Timezone + '-(1[0-2]|0[1-9])' + // Month + '-(3[01]|0[1-9]|[12][0-9])' + // Day + 'T(2[0-3]|[01][0-9])' + // Hour + ':([0-5][0-9])' + // Minute + ':([0-5][0-9])' + // Second + '(\\.[0-9]+)?' + // Fractional second + '(Z|[+-][01][0-9]:[0-5][0-9])?$', // Timezone 'g', ); const matches = [...date.matchAll(isoRegex)]; @@ -732,7 +758,7 @@ export default class LifecycleConfiguration { return errorInstances.InvalidArgument.customizeDescription(msg); } // Check for a timezone in the last match group. If none, add a Z to indicate UTC. - if (!matches[0][matches[0].length-1]) { + if (!matches[0][matches[0].length - 1]) { date += 'Z'; } const dateObj = new Date(date); @@ -740,11 +766,13 @@ export default class LifecycleConfiguration { const msg = 'Date is not a valid date'; return errorInstances.InvalidArgument.customizeDescription(msg); } - if (dateObj.getUTCHours() !== 0 - || dateObj.getUTCMinutes() !== 0 - || dateObj.getUTCSeconds() !== 0 - || dateObj.getUTCMilliseconds() !== 0) { - const msg = '\'Date\' must be at midnight GMT'; + if ( + dateObj.getUTCHours() !== 0 || + dateObj.getUTCMinutes() !== 0 || + dateObj.getUTCSeconds() !== 0 || + dateObj.getUTCMilliseconds() !== 0 + ) { + const msg = "'Date' must be at midnight GMT"; return errorInstances.InvalidArgument.customizeDescription(msg); } return null; @@ -768,11 +796,7 @@ export default class LifecycleConfiguration { * ] * } */ - _parseNoncurrentVersionTransition(rule: { - NoncurrentVersionTransition: any[]; - Prefix?: string[]; - Filter?: any; - }) { + _parseNoncurrentVersionTransition(rule: { NoncurrentVersionTransition: any[]; Prefix?: string[]; Filter?: any }) { const nonCurrentVersionTransition: { noncurrentDays: number; storageClass: string; @@ -780,8 +804,7 @@ export default class LifecycleConfiguration { const usedStorageClasses: string[] = []; for (let i = 0; i < rule.NoncurrentVersionTransition.length; i++) { const t = rule.NoncurrentVersionTransition[i]; // Transition object - const noncurrentDays: number | undefined = - t.NoncurrentDays && Number.parseInt(t.NoncurrentDays[0], 10); + const noncurrentDays: number | undefined = t.NoncurrentDays && Number.parseInt(t.NoncurrentDays[0], 10); const storageClass: string | undefined = t.StorageClass && t.StorageClass[0]; if (noncurrentDays === undefined || storageClass === undefined) { return { error: errors.MalformedXML }; @@ -828,14 +851,8 @@ export default class LifecycleConfiguration { * ] * } */ - _parseTransition(rule: { - Transition: any[]; - Prefix?: string[]; - Filter?: any; - }) { - const transition: - ({ days: number; storageClass: string } - | { date: string; storageClass: string })[] = []; + _parseTransition(rule: { Transition: any[]; Prefix?: string[]; Filter?: any }) { + const transition: ({ days: number; storageClass: string } | { date: string; storageClass: string })[] = []; const usedStorageClasses: string[] = []; let usedTimeType: string | null = null; for (let i = 0; i < rule.Transition.length; i++) { @@ -843,9 +860,11 @@ export default class LifecycleConfiguration { const days = t.Days && Number.parseInt(t.Days[0], 10); const date = t.Date && t.Date[0]; const storageClass = t.StorageClass && t.StorageClass[0]; - if ((days === undefined && date === undefined) || + if ( + (days === undefined && date === undefined) || (days !== undefined && date !== undefined) || - (storageClass === undefined)) { + storageClass === undefined + ) { return { error: errors.MalformedXML }; } let error = this._checkStorageClasses({ @@ -932,7 +951,7 @@ export default class LifecycleConfiguration { days?: number; date?: number; deleteMarker?: boolean; - newerNoncurrentVersions?: number + newerNoncurrentVersions?: number; }[]; } = { propName: 'actions', @@ -969,10 +988,10 @@ export default class LifecycleConfiguration { 'deleteMarker', 'transition', 'nonCurrentVersionTransition', - 'newerNoncurrentVersions' + 'newerNoncurrentVersions', ]; actionTimes.forEach(t => { - if (action[t]) { + if (action[t] != null) { a[t] = action[t]; } }); @@ -994,7 +1013,7 @@ export default class LifecycleConfiguration { * } */ _parseAbortIncompleteMultipartUpload(rule: { AbortIncompleteMultipartUpload: any[]; Filter?: any[] }) { - const abortObj: { error?: ArsenalError, days?: number } = {}; + const abortObj: { error?: ArsenalError; days?: number } = {}; let filter: any = null; if (rule.Filter && rule.Filter[0]) { if (rule.Filter[0].And) { @@ -1005,22 +1024,25 @@ export default class LifecycleConfiguration { } if (filter && filter.Tag) { abortObj.error = errorInstances.InvalidRequest.customizeDescription( - 'Tag-based filter cannot be used with ' + - 'AbortIncompleteMultipartUpload action', + 'Tag-based filter cannot be used with AbortIncompleteMultipartUpload action', ); return abortObj; } const subAbort = rule.AbortIncompleteMultipartUpload[0]; if (!subAbort.DaysAfterInitiation) { abortObj.error = errorInstances.MalformedXML.customizeDescription( - 'AbortIncompleteMultipartUpload action does not ' + - 'include DaysAfterInitiation'); + 'AbortIncompleteMultipartUpload action does not include DaysAfterInitiation', + ); return abortObj; } const daysInt = parseInt(subAbort.DaysAfterInitiation[0], 10); - if (daysInt < 1) { - abortObj.error = errorInstances.InvalidArgument.customizeDescription( - 'DaysAfterInitiation is not a positive integer'); + const error = this._checkDays({ + days: daysInt, + field: 'DaysAfterInitiation', + ancestor: 'AbortIncompleteMultipartUpload', + }); + if (error) { + abortObj.error = error; return abortObj; } abortObj.days = daysInt; @@ -1055,9 +1077,7 @@ export default class LifecycleConfiguration { return { error }; } const eodm = 'ExpiredObjectDeleteMarker'; - if (subExp.Date && - (subExp.Days || subExp[eodm]) || - (subExp.Days && subExp[eodm])) { + if ((subExp.Date && (subExp.Days || subExp[eodm])) || (subExp.Days && subExp[eodm])) { const msg = 'Expiration action includes more than one time'; const error = errorInstances.MalformedXML.customizeDescription(msg); return { error }; @@ -1072,9 +1092,9 @@ export default class LifecycleConfiguration { } if (subExp.Days) { const daysInt = parseInt(subExp.Days[0], 10); - if (daysInt < 1) { - expObj.error = errorInstances.InvalidArgument.customizeDescription( - 'Expiration days is not a positive integer'); + const error = this._checkDays({ days: daysInt, field: 'Days', ancestor: 'Expiration' }); + if (error) { + expObj.error = error; } else { expObj.days = daysInt; } @@ -1090,14 +1110,15 @@ export default class LifecycleConfiguration { } if (filter && filter.Tag) { expObj.error = errorInstances.InvalidRequest.customizeDescription( - 'Tag-based filter cannot be used with ' + - 'ExpiredObjectDeleteMarker action'); + 'Tag-based filter cannot be used with ExpiredObjectDeleteMarker action', + ); return expObj; } const validValues = ['true', 'false']; if (!validValues.includes(subExp.ExpiredObjectDeleteMarker[0])) { expObj.error = errorInstances.MalformedXML.customizeDescription( - 'ExpiredObjDeleteMarker is not true or false'); + 'ExpiredObjDeleteMarker is not true or false', + ); } else { expObj.deleteMarker = subExp.ExpiredObjectDeleteMarker[0]; } @@ -1122,28 +1143,29 @@ export default class LifecycleConfiguration { const subNVExp = rule.NoncurrentVersionExpiration[0]; if (!subNVExp.NoncurrentDays) { const error = errorInstances.MalformedXML.customizeDescription( - 'NoncurrentVersionExpiration action does not include ' + - 'NoncurrentDays'); + 'NoncurrentVersionExpiration action does not include NoncurrentDays', + ); return { error }; } const actionParams: { error?: ArsenalError; days: number; - newerNoncurrentVersions: number; + newerNoncurrentVersions?: number; } = { days: 0, - newerNoncurrentVersions: 0, }; const daysInt = parseInt(subNVExp.NoncurrentDays[0], 10); - if (daysInt < 1) { - const msg = 'NoncurrentDays is not a positive integer'; - const error = errorInstances.InvalidArgument.customizeDescription(msg); + const error = this._checkDays({ + days: daysInt, + field: 'NoncurrentDays', + ancestor: 'NoncurrentVersionExpiration', + }); + if (error) { return { error }; - } else { - actionParams.days = daysInt; } + actionParams.days = daysInt; if (subNVExp.NewerNoncurrentVersions) { const newerVersionsInt = parseInt(subNVExp.NewerNoncurrentVersions[0], 10); @@ -1155,9 +1177,6 @@ export default class LifecycleConfiguration { } actionParams.newerNoncurrentVersions = newerVersionsInt; - - } else { - actionParams.newerNoncurrentVersions = 0; } return actionParams; @@ -1194,7 +1213,7 @@ export default class LifecycleConfiguration { } actions.forEach((a: any) => { assert.strictEqual(typeof a.actionName, 'string'); - if (a.days) { + if (a.days !== undefined) { assert.strictEqual(typeof a.days, 'number'); } if (a.date) { @@ -1204,8 +1223,7 @@ export default class LifecycleConfiguration { assert.strictEqual(typeof a.deleteMarker, 'string'); } if (a.nonCurrentVersionTransition) { - assert.strictEqual( - typeof a.nonCurrentVersionTransition, 'object'); + assert.strictEqual(typeof a.nonCurrentVersionTransition, 'object'); a.nonCurrentVersionTransition.forEach(t => { assert.strictEqual(typeof t.noncurrentDays, 'number'); assert.strictEqual(typeof t.storageClass, 'string'); @@ -1238,114 +1256,120 @@ export default class LifecycleConfiguration { */ static getConfigXml(config: { rules: Rule[] }) { const rules = config.rules; - const rulesXML = rules.map(rule => { - const { ruleID, ruleStatus, filter, actions, prefix } = rule; - const ID = `${escapeForXml(ruleID)}`; - const Status = `${ruleStatus}`; - let rulePrefix: string | undefined; - if (prefix !== undefined) { - rulePrefix = prefix; - } else { - rulePrefix = filter?.rulePrefix; - } - const tags = filter && filter.tags; - const Prefix = rulePrefix !== undefined ? - `${escapeForXml(rulePrefix)}` : ''; - let tagXML = ''; - if (tags) { - tagXML = tags.map(t => { - const { key, val } = t; - const Tag = `${escapeForXml(key)}` + - `${escapeForXml(val)}`; - return Tag; - }).join(''); - } - let Filter: string; - if (prefix !== undefined) { - // Prefix is in the top-level of the config, so we can skip the - // filter property. - Filter = Prefix; - } else if (filter?.rulePrefix !== undefined && !tags) { - Filter = `${Prefix}`; - } else if (tags && - (filter.rulePrefix !== undefined || tags.length > 1)) { - Filter = `${Prefix}${tagXML}`; - } else { - // remaining condition is if only one or no tag - Filter = `${tagXML}`; - } - - const Actions = actions.map(action => { - const { - actionName, - days, - date, - deleteMarker, - nonCurrentVersionTransition, - transition, - newerNoncurrentVersions, - } = action; - let Action: any; - if (actionName === 'AbortIncompleteMultipartUpload') { - Action = `<${actionName}>${days}` + - ``; - } else if (actionName === 'NoncurrentVersionExpiration') { - const Days = `${days}`; - const NewerVersions = newerNoncurrentVersions ? - `${newerNoncurrentVersions}` : ''; - Action = `<${actionName}>${Days}${NewerVersions}`; - } else if (actionName === 'Expiration') { - const Days = days ? `${days}` : ''; - const Date = date ? `${date}` : ''; - const DelMarker = deleteMarker ? - `${deleteMarker}` + - '' : ''; - Action = `<${actionName}>${Days}${Date}${DelMarker}` + - ``; + const rulesXML = rules + .map(rule => { + const { ruleID, ruleStatus, filter, actions, prefix } = rule; + const ID = `${escapeForXml(ruleID)}`; + const Status = `${ruleStatus}`; + let rulePrefix: string | undefined; + if (prefix !== undefined) { + rulePrefix = prefix; + } else { + rulePrefix = filter?.rulePrefix; } - if (actionName === 'NoncurrentVersionTransition') { - const xml: string[] = []; - nonCurrentVersionTransition!.forEach(transition => { - const { noncurrentDays, storageClass } = transition; - xml.push( - `<${actionName}>`, - `${noncurrentDays}` + - '', - `${storageClass}`, - ``, - ); - }); - Action = xml.join(''); + const tags = filter && filter.tags; + const Prefix = rulePrefix !== undefined ? `${escapeForXml(rulePrefix)}` : ''; + let tagXML = ''; + if (tags) { + tagXML = tags + .map(t => { + const { key, val } = t; + const Tag = + `${escapeForXml(key)}` + `${escapeForXml(val)}`; + return Tag; + }) + .join(''); + } + let Filter: string; + if (prefix !== undefined) { + // Prefix is in the top-level of the config, so we can skip the + // filter property. + Filter = Prefix; + } else if (filter?.rulePrefix !== undefined && !tags) { + Filter = `${Prefix}`; + } else if (tags && (filter.rulePrefix !== undefined || tags.length > 1)) { + Filter = `${Prefix}${tagXML}`; + } else { + // remaining condition is if only one or no tag + Filter = `${tagXML}`; } - if (actionName === 'Transition') { - const xml: string[] = []; - transition!.forEach(transition => { - const { days, date, storageClass } = transition; - let element: string = ''; - if (days !== undefined) { - element = `${days}`; + + const Actions = actions + .map(action => { + const { + actionName, + days, + date, + deleteMarker, + nonCurrentVersionTransition, + transition, + newerNoncurrentVersions, + } = action; + let Action: any; + if (actionName === 'AbortIncompleteMultipartUpload') { + Action = + `<${actionName}>${days}` + + ``; + } else if (actionName === 'NoncurrentVersionExpiration') { + const Days = `${days}`; + const NewerVersions = newerNoncurrentVersions + ? `${newerNoncurrentVersions}` + : ''; + Action = `<${actionName}>${Days}${NewerVersions}`; + } else if (actionName === 'Expiration') { + const Days = days !== undefined ? `${days}` : ''; + const Date = date ? `${date}` : ''; + const DelMarker = deleteMarker + ? `${deleteMarker}` + '' + : ''; + Action = `<${actionName}>${Days}${Date}${DelMarker}` + ``; } - if (date !== undefined) { - element = `${date}`; + if (actionName === 'NoncurrentVersionTransition') { + const xml: string[] = []; + nonCurrentVersionTransition!.forEach(transition => { + const { noncurrentDays, storageClass } = transition; + xml.push( + `<${actionName}>`, + `${noncurrentDays}` + '', + `${storageClass}`, + ``, + ); + }); + Action = xml.join(''); } - xml.push( - `<${actionName}>`, - element, - `${storageClass}`, - ``, - ); - }); - Action = xml.join(''); - } - return Action; - }).join(''); - return `${ID}${Status}${Filter}${Actions}`; - }).join(''); - return '' + - ' { + const { days, date, storageClass } = transition; + let element: string = ''; + if (days !== undefined) { + element = `${days}`; + } + if (date !== undefined) { + element = `${date}`; + } + xml.push( + `<${actionName}>`, + element, + `${storageClass}`, + ``, + ); + }); + Action = xml.join(''); + } + return Action; + }) + .join(''); + return `${ID}${Status}${Filter}${Actions}`; + }) + .join(''); + return ( + '' + + '' + `${rulesXML}` + - ''; + '' + ); } /** diff --git a/lib/s3middleware/lifecycleHelpers/LifecycleUtils.ts b/lib/s3middleware/lifecycleHelpers/LifecycleUtils.ts index 76e9321d4..e35fd28b9 100644 --- a/lib/s3middleware/lifecycleHelpers/LifecycleUtils.ts +++ b/lib/s3middleware/lifecycleHelpers/LifecycleUtils.ts @@ -34,29 +34,17 @@ export default class LifecycleUtils { } /** - * Compare two transition rules and return the one that is most recent. - * @param params - The function parameters - * @param params.transition1 - A transition from the current rule - * @param params.transition2 - A transition from the previous rule - * @param params.lastModified - The object's last modified - * date - * @return The most applicable transition rule - */ - compareTransitions(params: { - lastModified: string; - transition1: Transition; - transition2?: Transition; - }): Transition; - compareTransitions(params: { - lastModified: string; - transition1?: Transition; - transition2: Transition; - }): Transition; - compareTransitions(params: { - lastModified: string; - transition1?: Transition; - transition2?: Transition; - }) { + * Compare two transition rules and return the one that is most recent. + * @param params - The function parameters + * @param params.transition1 - A transition from the current rule + * @param params.transition2 - A transition from the previous rule + * @param params.lastModified - The object's last modified + * date + * @return The most applicable transition rule + */ + compareTransitions(params: { lastModified: string; transition1: Transition; transition2?: Transition }): Transition; + compareTransitions(params: { lastModified: string; transition1?: Transition; transition2: Transition }): Transition; + compareTransitions(params: { lastModified: string; transition1?: Transition; transition2?: Transition }) { const { transition1, transition2, lastModified } = params; if (transition1 === undefined) { return transition2; @@ -70,14 +58,14 @@ export default class LifecycleUtils { } /** - * Compare two non-current version transition rules and return the one that is most recent. - * @param params - The function parameters - * @param params.transition1 - A non-current version transition from the current rule - * @param params.transition2 - A non-current version transition from the previous rule - * @param params.lastModified - The object's last modified - * date - * @return The most applicable transition rule - */ + * Compare two non-current version transition rules and return the one that is most recent. + * @param params - The function parameters + * @param params.transition1 - A non-current version transition from the current rule + * @param params.transition2 - A non-current version transition from the previous rule + * @param params.lastModified - The object's last modified + * date + * @return The most applicable transition rule + */ compareNCVTransitions(params: { lastModified: string; transition1?: NoncurrentVersionTransition; @@ -107,20 +95,15 @@ export default class LifecycleUtils { // TODO Fix This /** - * Find the most relevant trantition rule for the given transitions array - * and any previously stored transition from another rule. - * @param params - The function parameters - * @param params.transitions - Array of lifecycle rule transitions - * @param params.lastModified - The object's last modified - * date - * @return The most applicable transition rule - */ - getApplicableTransition(params: { - store: Store; - currentDate: Date; - transitions: any[]; - lastModified: string; - }) { + * Find the most relevant trantition rule for the given transitions array + * and any previously stored transition from another rule. + * @param params - The function parameters + * @param params.transitions - Array of lifecycle rule transitions + * @param params.lastModified - The object's last modified + * date + * @return The most applicable transition rule + */ + getApplicableTransition(params: { store: Store; currentDate: Date; transitions: any[]; lastModified: string }) { const { transitions, store, lastModified, currentDate } = params; const transition = transitions.reduce((result, transition) => { const isApplicable = // Is the transition time in the past? @@ -143,24 +126,19 @@ export default class LifecycleUtils { } /** - * Find the most relevant non-current version transition rule for the given transitions array - * and any previously stored non-current version transition from another rule. - * @param params - The function parameters - * @param params.transitions - Array of lifecycle non-current version transitions - * @param params.lastModified - The object's last modified - * date - * @return The most applicable non-current version transition rule - */ - getApplicableNCVTransition(params: { - store: Store; - currentDate: Date; - transitions: any[]; - lastModified: string; - }) { + * Find the most relevant non-current version transition rule for the given transitions array + * and any previously stored non-current version transition from another rule. + * @param params - The function parameters + * @param params.transitions - Array of lifecycle non-current version transitions + * @param params.lastModified - The object's last modified + * date + * @return The most applicable non-current version transition rule + */ + getApplicableNCVTransition(params: { store: Store; currentDate: Date; transitions: any[]; lastModified: string }) { const { transitions, store, lastModified, currentDate } = params; const transition = transitions.reduce((result, transition) => { const isApplicable = // Is the transition time in the past? - this._datetime.getTimestamp(currentDate) >= + this._datetime.getTimestamp(currentDate) >= this._datetime.getNCVTransitionTimestamp(transition, lastModified)!; if (!isApplicable) { return result; @@ -180,12 +158,12 @@ export default class LifecycleUtils { // TODO /** - * Filter out all rules based on `Status` and `Filter` (Prefix and Tags) - * @param bucketLCRules - array of bucket lifecycle rules - * @param item - represents a single object, version, or upload - * @param objTags - all tags for given `item` - * @return list of all filtered rules that apply to `item` - */ + * Filter out all rules based on `Status` and `Filter` (Prefix and Tags) + * @param bucketLCRules - array of bucket lifecycle rules + * @param item - represents a single object, version, or upload + * @param objTags - all tags for given `item` + * @return list of all filtered rules that apply to `item` + */ filterRules(bucketLCRules: any[], item: any, objTags: any) { /* Bucket Tags must be included in the list of object tags. @@ -220,19 +198,15 @@ export default class LifecycleUtils { // console.log(rule.Prefix); // console.log(rule.Filter); // console.log(rule.Filter.And); - const prefix = rule.Prefix - || (rule.Filter && (rule.Filter.And - ? rule.Filter.And.Prefix - : rule.Filter.Prefix)); + const prefix = + rule.Prefix || (rule.Filter && (rule.Filter.And ? rule.Filter.And.Prefix : rule.Filter.Prefix)); if (prefix && !item.Key.startsWith(prefix)) { return false; } if (!rule.Filter) { return true; } - const tags = rule.Filter.And - ? rule.Filter.And.Tags - : (rule.Filter.Tag && [rule.Filter.Tag]); + const tags = rule.Filter.And ? rule.Filter.And.Tags : rule.Filter.Tag && [rule.Filter.Tag]; if (tags && !deepCompare(tags, objTags.TagSet || [])) { return false; } @@ -242,14 +216,14 @@ export default class LifecycleUtils { // TODO /** - * For all filtered rules, get rules that apply the earliest - * @param rules - list of filtered rules that apply to a specific - * object, version, or upload - * @param metadata - metadata about the object to transition - * @return all applicable rules with earliest dates of action - * i.e. { Expiration: { Date: , Days: 10 }, - * NoncurrentVersionExpiration: { NoncurrentDays: 5 } } - */ + * For all filtered rules, get rules that apply the earliest + * @param rules - list of filtered rules that apply to a specific + * object, version, or upload + * @param metadata - metadata about the object to transition + * @return all applicable rules with earliest dates of action + * i.e. { Expiration: { Date: , Days: 10 }, + * NoncurrentVersionExpiration: { NoncurrentDays: 5 } } + */ getApplicableRules(rules: LifecycleRuleData[], metadata: any) { // Declare the current date before the reducing function so that all // rule comparisons use the same date. @@ -257,8 +231,8 @@ export default class LifecycleUtils { const applicableRules = rules.reduce((store: Store, rule) => { // filter and find earliest dates if (rule.Expiration && this._supportedRules.includes('Expiration')) { - if (rule.Expiration.Days) { - if (!store.Expiration?.Days || rule.Expiration.Days < store.Expiration.Days) { + if (rule.Expiration.Days !== undefined) { + if (store.Expiration?.Days === undefined || rule.Expiration.Days < store.Expiration.Days) { store.Expiration = { ...store.Expiration, ID: rule.ID, @@ -293,8 +267,8 @@ export default class LifecycleUtils { const ncd = 'NoncurrentDays'; const nncv = 'NewerNoncurrentVersions'; - if (ncvExpiration[ncd]) { - if (!store[ncve]?.[ncd] || ncvExpiration[ncd] < store[ncve][ncd]) { + if (ncvExpiration[ncd] != null) { + if (store[ncve]?.[ncd] == null || ncvExpiration[ncd] < store[ncve][ncd]) { store[ncve] = { ...store[ncve], ID: rule.ID, @@ -305,13 +279,15 @@ export default class LifecycleUtils { } } - if (rule.AbortIncompleteMultipartUpload - && this._supportedRules.includes('AbortIncompleteMultipartUpload')) { + if ( + rule.AbortIncompleteMultipartUpload && + this._supportedRules.includes('AbortIncompleteMultipartUpload') + ) { // Names are long, so obscuring a bit const aimu = 'AbortIncompleteMultipartUpload'; const dai = 'DaysAfterInitiation'; - if (!store[aimu]?.[dai] || rule[aimu][dai] < store[aimu][dai]) { + if (store[aimu]?.[dai] === undefined || rule[aimu][dai] < store[aimu][dai]) { store[aimu] = { ...store[aimu], ID: rule.ID, @@ -345,13 +321,14 @@ export default class LifecycleUtils { }, {}); // Do not transition to a location where the object is already stored. - if (applicableRules.Transition - && applicableRules.Transition.StorageClass === metadata.StorageClass) { + if (applicableRules.Transition && applicableRules.Transition.StorageClass === metadata.StorageClass) { applicableRules.Transition = undefined; } - if (applicableRules.NoncurrentVersionTransition - && applicableRules.NoncurrentVersionTransition.StorageClass === metadata.StorageClass) { + if ( + applicableRules.NoncurrentVersionTransition && + applicableRules.NoncurrentVersionTransition.StorageClass === metadata.StorageClass + ) { applicableRules.NoncurrentVersionTransition = undefined; } diff --git a/package.json b/package.json index 0be4ca501..973da04e6 100644 --- a/package.json +++ b/package.json @@ -3,7 +3,7 @@ "engines": { "node": ">=20" }, - "version": "8.5.5", + "version": "8.5.6", "config": { "mongodbMemoryServer": { "version": "8.0.23" diff --git a/tests/unit/models/LifecycleConfiguration.spec.js b/tests/unit/models/LifecycleConfiguration.spec.js index ff48b8ed4..0278a70e9 100644 --- a/tests/unit/models/LifecycleConfiguration.spec.js +++ b/tests/unit/models/LifecycleConfiguration.spec.js @@ -2,8 +2,10 @@ const assert = require('assert'); const { parseString } = require('xml2js'); const { ValidLifecycleRules } = require('../../../lib/models/LifecycleConfiguration'); -const LifecycleConfiguration = - require('../../../lib/models/LifecycleConfiguration').default; +const LifecycleConfiguration = require('../../../lib/models/LifecycleConfiguration').default; +const DummyRequestLogger = require('../helpers').DummyRequestLogger; + +const log = new DummyRequestLogger(); const days = { AbortIncompleteMultipartUpload: 'DaysAfterInitiation', @@ -12,15 +14,11 @@ const days = { }; const mockConfig = { - replicationEndpoints: [ - { site: 'a' }, - { site: 'b' }, - { site: 'aCrrLocation' } - ], + replicationEndpoints: [{ site: 'a' }, { site: 'b' }, { site: 'aCrrLocation' }], locationConstraints: { - 'a': { isCRR: false }, - 'b': { isCRR: false }, - 'aCrrLocation': { isCRR: true }, // true => 'aCrrLocation' will be filtered out from storageClasses + a: { isCRR: false }, + b: { isCRR: false }, + aCrrLocation: { isCRR: true }, // true => 'aCrrLocation' will be filtered out from storageClasses }, supportedLifecycleRules: ValidLifecycleRules, }; @@ -62,71 +60,156 @@ date.setUTCHours(0, 0, 0, 0); */ const requiredTags = [ - { tag: 'LifecycleConfiguration', error: 'MalformedXML', - errMessage: 'request xml is undefined or empty' }, - { tag: 'Rule', error: 'MissingRequiredParameter', - errMessage: 'missing required key \'Rules\' in ' + - 'LifecycleConfiguration' }, - { tag: 'Status', error: 'MissingRequiredParameter', - errMessage: 'Rule xml does not include Status' }, - { tag: 'Filter', error: 'MalformedXML', - errMessage: 'Rule xml does not include valid Filter or Prefix' }, - { tag: 'Action', error: 'InvalidRequest', - errMessage: 'Rule does not include valid action' }]; + { tag: 'LifecycleConfiguration', error: 'MalformedXML', errMessage: 'request xml is undefined or empty' }, + { + tag: 'Rule', + error: 'MissingRequiredParameter', + errMessage: "missing required key 'Rules' in " + 'LifecycleConfiguration', + }, + { tag: 'Status', error: 'MissingRequiredParameter', errMessage: 'Rule xml does not include Status' }, + { tag: 'Filter', error: 'MalformedXML', errMessage: 'Rule xml does not include valid Filter or Prefix' }, + { tag: 'Action', error: 'InvalidRequest', errMessage: 'Rule does not include valid action' }, +]; const invalidActions = [ - { tag: 'AbortIncompleteMultipartUpload', label: 'no-time', + { + tag: 'AbortIncompleteMultipartUpload', + label: 'no-time', error: 'MalformedXML', - errMessage: 'AbortIncompleteMultipartUpload action does not ' + - 'include DaysAfterInitiation' }, - { tag: 'AbortIncompleteMultipartUpload', label: 'no-tags', - error: 'InvalidRequest', errMessage: 'Tag-based filter cannot be ' + - 'used with AbortIncompleteMultipartUpload action' }, - { tag: 'AbortIncompleteMultipartUpload', label: 'invalid-days', + errMessage: 'AbortIncompleteMultipartUpload action does not ' + 'include DaysAfterInitiation', + }, + { + tag: 'AbortIncompleteMultipartUpload', + label: 'no-tags', + error: 'InvalidRequest', + errMessage: 'Tag-based filter cannot be ' + 'used with AbortIncompleteMultipartUpload action', + }, + { + tag: 'AbortIncompleteMultipartUpload', + label: 'invalid-days', error: 'InvalidArgument', - errMessage: 'DaysAfterInitiation is not a positive integer' }, - { tag: 'Expiration', label: 'no-time', error: 'MalformedXML', - errMessage: 'Expiration action does not include an action time' }, - { tag: 'Expiration', label: 'mult-times', error: 'MalformedXML', - errMessage: 'Expiration action includes more than one time' }, - { tag: 'Expiration', label: 'non-iso', error: 'InvalidArgument', - errMessage: 'Date must be in ISO 8601 format' }, - { tag: 'Expiration', label: 'invalid-days', error: 'InvalidArgument', - errMessage: 'Expiration days is not a positive integer' }, - { tag: 'Expiration', label: 'no-tags', inTag: 'ExpiredObjectDeleteMarker', + errMessage: "'DaysAfterInitiation' for AbortIncompleteMultipartUpload action must be a positive integer", + }, + { + tag: 'AbortIncompleteMultipartUpload', + label: 'exceed-max-days', + error: 'MalformedXML', + errMessage: "'DaysAfterInitiation' in AbortIncompleteMultipartUpload " + 'action must not exceed 2147483647', + }, + { + tag: 'Expiration', + label: 'no-time', + error: 'MalformedXML', + errMessage: 'Expiration action does not include an action time', + }, + { + tag: 'Expiration', + label: 'mult-times', + error: 'MalformedXML', + errMessage: 'Expiration action includes more than one time', + }, + { tag: 'Expiration', label: 'non-iso', error: 'InvalidArgument', errMessage: 'Date must be in ISO 8601 format' }, + { + tag: 'Expiration', + label: 'invalid-days', + error: 'InvalidArgument', + errMessage: "'Days' for Expiration action must be a positive integer", + }, + { + tag: 'Expiration', + label: 'exceed-max-days', + error: 'MalformedXML', + errMessage: "'Days' in Expiration action must not exceed 2147483647", + }, + { + tag: 'Expiration', + label: 'no-tags', + inTag: 'ExpiredObjectDeleteMarker', error: 'InvalidRequest', - errMessage: 'Tag-based filter cannot be used with ' + - 'ExpiredObjectDeleteMarker action' }, - { tag: 'Expiration', label: 'invalid-eodm', error: 'MalformedXML', - errMessage: 'ExpiredObjDeleteMarker is not true or false' }, - { tag: 'NoncurrentVersionExpiration', label: 'no-time', + errMessage: 'Tag-based filter cannot be used with ' + 'ExpiredObjectDeleteMarker action', + }, + { + tag: 'Expiration', + label: 'invalid-eodm', + error: 'MalformedXML', + errMessage: 'ExpiredObjDeleteMarker is not true or false', + }, + { + tag: 'NoncurrentVersionExpiration', + label: 'no-time', error: 'MalformedXML', - errMessage: 'NoncurrentVersionExpiration action does not include ' + - 'NoncurrentDays' }, - { tag: 'NoncurrentVersionExpiration', label: 'invalid-days', + errMessage: 'NoncurrentVersionExpiration action does not include ' + 'NoncurrentDays', + }, + { + tag: 'NoncurrentVersionExpiration', + label: 'invalid-days', error: 'InvalidArgument', - errMessage: 'NoncurrentDays is not a positive integer' }]; + errMessage: "'NoncurrentDays' for NoncurrentVersionExpiration action must be a positive integer", + }, + { + tag: 'NoncurrentVersionExpiration', + label: 'exceed-max-days', + error: 'MalformedXML', + errMessage: "'NoncurrentDays' in NoncurrentVersionExpiration " + 'action must not exceed 2147483647', + }, +]; const invalidFilters = [ - { tag: 'Filter', label: 'also-prefix', error: 'MalformedXML', - errMessage: 'Rule xml should not include both Filter and Prefix' }, - { tag: 'Filter', label: 'and-prefix-tag', error: 'MalformedXML', - errMessage: 'Filter should only include one of And, Prefix, or ' + - 'Tag key' }, - { tag: 'And', label: 'only-prefix', error: 'MalformedXML', - errMessage: 'And should include Prefix and Tags or more than one Tag' }, - { tag: 'And', label: 'single-tag', error: 'MalformedXML', - errMessage: 'And should include Prefix and Tags or more than one Tag' }, - { tag: 'Tag', label: 'no-key', error: 'MissingRequiredParameter', - errMessage: 'Tag XML does not contain both Key and Value' }, - { tag: 'Tag', label: 'no-value', error: 'MissingRequiredParameter', - errMessage: 'Tag XML does not contain both Key and Value' }, - { tag: 'Tag', label: 'key-too-long', error: 'InvalidRequest', - errMessage: 'A Tag\'s Key must be a length between 1 and 128' }, - { tag: 'Tag', label: 'value-too-long', error: 'InvalidRequest', - errMessage: 'A Tag\'s Value must be a length between 0 and 256' }, - { tag: 'Tag', label: 'prefix-too-long', error: 'InvalidRequest', - errMessage: 'The maximum size of a prefix is 1024' }]; + { + tag: 'Filter', + label: 'also-prefix', + error: 'MalformedXML', + errMessage: 'Rule xml should not include both Filter and Prefix', + }, + { + tag: 'Filter', + label: 'and-prefix-tag', + error: 'MalformedXML', + errMessage: 'Filter should only include one of And, Prefix, or ' + 'Tag key', + }, + { + tag: 'And', + label: 'only-prefix', + error: 'MalformedXML', + errMessage: 'And should include Prefix and Tags or more than one Tag', + }, + { + tag: 'And', + label: 'single-tag', + error: 'MalformedXML', + errMessage: 'And should include Prefix and Tags or more than one Tag', + }, + { + tag: 'Tag', + label: 'no-key', + error: 'MissingRequiredParameter', + errMessage: 'Tag XML does not contain both Key and Value', + }, + { + tag: 'Tag', + label: 'no-value', + error: 'MissingRequiredParameter', + errMessage: 'Tag XML does not contain both Key and Value', + }, + { + tag: 'Tag', + label: 'key-too-long', + error: 'InvalidRequest', + errMessage: "A Tag's Key must be a length between 1 and 128", + }, + { + tag: 'Tag', + label: 'value-too-long', + error: 'InvalidRequest', + errMessage: "A Tag's Value must be a length between 0 and 256", + }, + { + tag: 'Tag', + label: 'prefix-too-long', + error: 'InvalidRequest', + errMessage: 'The maximum size of a prefix is 1024', + }, +]; function generateAction(errorTag, tagObj) { const xmlObj = {}; @@ -136,14 +219,16 @@ function generateAction(errorTag, tagObj) { middleTags = ''; } if (tagObj.label === 'no-tags') { - middleTags = tagObj.inTag ? - `<${tagObj.inTag}>true` : - `<${days[tagObj.tag]}>1`; - xmlObj.filter = 'key' + - ''; + middleTags = tagObj.inTag + ? `<${tagObj.inTag}>true` + : `<${days[tagObj.tag]}>1`; + xmlObj.filter = 'key' + ''; } if (tagObj.label === 'invalid-days') { - middleTags = `<${days[tagObj.tag]}>0`; + middleTags = `<${days[tagObj.tag]}>-1`; + } + if (tagObj.label === 'exceed-max-days') { + middleTags = `<${days[tagObj.tag]}>${MAX_DAYS + 1}`; } if (tagObj.label === 'mult-times') { middleTags = `1${date}`; @@ -152,8 +237,7 @@ function generateAction(errorTag, tagObj) { middleTags = '03-08-2018'; } if (tagObj.label === 'invalid-eodm') { - middleTags = 'foo' + - ''; + middleTags = 'foo' + ''; } xmlObj.actions = `<${tagObj.tag}>${middleTags}`; } else { @@ -197,15 +281,16 @@ function generateFilter(errorTag, tagObj) { middleTags = `${longValue}`; } if (tagObj.label === 'mult-prefixes') { - middleTags = 'foobar' + - `${tagObj.lastPrefix}`; + middleTags = 'foobar' + `${tagObj.lastPrefix}`; } if (tagObj.label === 'mult-tags') { - middleTags = 'colorblue' + + middleTags = + 'colorblue' + 'shapecircle'; } if (tagObj.label === 'not-unique-key-tag') { - middleTags = 'colorblue' + + middleTags = + 'colorblue' + 'colorred'; } Filter = `${middleTags}`; @@ -221,8 +306,7 @@ function generateFilter(errorTag, tagObj) { function generateRule(errorTag, tagObj, ID, Status, Filter, Action) { let Rule; if (tagObj && tagObj.rule === 'not-unique-id') { - Rule = `${ID + Status + Filter + Action}` + - `${ID + Status + Filter + Action}`; + Rule = `${ID + Status + Filter + Action}` + `${ID + Status + Filter + Action}`; } else if (tagObj && tagObj.rule === 'too-many-rules') { for (let i = 0; i <= 1000; i++) { // eslint-disable-next-line no-param-reassign @@ -247,8 +331,7 @@ function generateXml(errorTag, tagObj) { ID = 'foo'; } if (errorTag === 'Status') { - Status = tagObj && tagObj.status ? - `${tagObj.status}` : ''; + Status = tagObj && tagObj.status ? `${tagObj.status}` : ''; } else { Status = 'Enabled'; } @@ -269,9 +352,8 @@ function generateXml(errorTag, tagObj) { } else { Rule = `${ID + Status + Filter + Action}`; } - const Lifecycle = errorTag === 'LifecycleConfiguration' ? '' : - `${Rule}` + - ''; + const Lifecycle = + errorTag === 'LifecycleConfiguration' ? '' : `${Rule}` + ''; return Lifecycle; } @@ -284,8 +366,7 @@ function generateParsedXml(errorTag, tagObj, cb) { } function checkError(parsedXml, error, errMessage, cb) { - const lcConfig = new LifecycleConfiguration(parsedXml, mockConfig) - .getLifecycleConfiguration(); + const lcConfig = new LifecycleConfiguration(parsedXml, log, mockConfig).getLifecycleConfiguration(); assert.strictEqual(lcConfig.error.is[error], true); assert.strictEqual(lcConfig.error.description, errMessage); cb(); @@ -304,30 +385,27 @@ describe('LifecycleConfiguration class getLifecycleConfiguration', () => { }); requiredTags.forEach(t => { - it(`should return ${t.error} error if ${t.tag} tag is missing`, - done => { - generateParsedXml(t.tag, null, parsedXml => { - checkError(parsedXml, t.error, t.errMessage, done); - }); + it(`should return ${t.error} error if ${t.tag} tag is missing`, done => { + generateParsedXml(t.tag, null, parsedXml => { + checkError(parsedXml, t.error, t.errMessage, done); }); + }); }); invalidActions.forEach(a => { - it(`should return ${a.error} for ${a.label} action error`, - done => { - generateParsedXml('Action', a, parsedXml => { - checkError(parsedXml, a.error, a.errMessage, done); - }); + it(`should return ${a.error} for ${a.label} action error`, done => { + generateParsedXml('Action', a, parsedXml => { + checkError(parsedXml, a.error, a.errMessage, done); }); + }); }); invalidFilters.forEach(filter => { - it(`should return ${filter.error} for ${filter.label} filter error`, - done => { - generateParsedXml('Filter', filter, parsedXml => { - checkError(parsedXml, filter.error, filter.errMessage, done); - }); + it(`should return ${filter.error} for ${filter.label} filter error`, done => { + generateParsedXml('Filter', filter, parsedXml => { + checkError(parsedXml, filter.error, filter.errMessage, done); }); + }); }); it('should return MalformedXML error if invalid status', done => { @@ -365,10 +443,11 @@ describe('LifecycleConfiguration class getLifecycleConfiguration', () => { it('should apply all unique Key tags if multiple tags included', done => { tagObj.label = 'mult-tags'; generateParsedXml('Filter', tagObj, parsedXml => { - const lcConfig = new LifecycleConfiguration(parsedXml, mockConfig) - .getLifecycleConfiguration(); - const expected = [{ key: 'color', val: 'blue' }, - { key: 'shape', val: 'circle' }]; + const lcConfig = new LifecycleConfiguration(parsedXml, log, mockConfig).getLifecycleConfiguration(); + const expected = [ + { key: 'color', val: 'blue' }, + { key: 'shape', val: 'circle' }, + ]; assert.deepStrictEqual(expected, lcConfig.rules[0].filter.tags); done(); }); @@ -386,10 +465,8 @@ describe('LifecycleConfiguration class getLifecycleConfiguration', () => { tagObj.label = 'empty-prefix'; const expectedPrefix = ''; generateParsedXml('Filter', tagObj, parsedXml => { - const lcConfig = new LifecycleConfiguration(parsedXml, mockConfig). - getLifecycleConfiguration(); - assert.strictEqual(expectedPrefix, - lcConfig.rules[0].filter.rulePrefix); + const lcConfig = new LifecycleConfiguration(parsedXml, log, mockConfig).getLifecycleConfiguration(); + assert.strictEqual(expectedPrefix, lcConfig.rules[0].filter.rulePrefix); done(); }); }); @@ -410,8 +487,7 @@ describe('LifecycleConfiguration class getLifecycleConfiguration', () => { `; parseString(xml, (err, parsedXml) => { assert.equal(err, null, 'Error parsing xml'); - const lcConfig = new LifecycleConfiguration(parsedXml, mockConfig) - .getLifecycleConfiguration(); + const lcConfig = new LifecycleConfiguration(parsedXml, log, mockConfig).getLifecycleConfiguration(); assert.strictEqual(lcConfig.error.is.NotImplemented, true); assert.strictEqual(lcConfig.error.description, 'Transition lifecycle action not implemented'); done(); @@ -437,8 +513,7 @@ describe('LifecycleConfiguration class getLifecycleConfiguration', () => { `; parseString(xml, (err, parsedXml) => { assert.equal(err, null, 'Error parsing xml'); - const lcConfig = new LifecycleConfiguration(parsedXml, mockConfig) - .getLifecycleConfiguration(); + const lcConfig = new LifecycleConfiguration(parsedXml, log, mockConfig).getLifecycleConfiguration(); assert.strictEqual(lcConfig.error.is.NotImplemented, true); assert.strictEqual(lcConfig.error.description, 'Transition lifecycle action not implemented'); done(); @@ -464,8 +539,7 @@ describe('LifecycleConfiguration class getLifecycleConfiguration', () => { `; parseString(xml, (err, parsedXml) => { assert.equal(err, null, 'Error parsing xml'); - const lcConfig = new LifecycleConfiguration(parsedXml, mockConfig) - .getLifecycleConfiguration(); + const lcConfig = new LifecycleConfiguration(parsedXml, log, mockConfig).getLifecycleConfiguration(); assert.strictEqual(lcConfig.error, undefined); done(); }); @@ -487,29 +561,240 @@ describe('LifecycleConfiguration class getLifecycleConfiguration', () => { `; parseString(xml, (err, parsedXml) => { assert.equal(err, null, 'Error parsing xml'); - const lcConfig = new LifecycleConfiguration(parsedXml, mockConfig) - .getLifecycleConfiguration(); + const lcConfig = new LifecycleConfiguration(parsedXml, log, mockConfig).getLifecycleConfiguration(); assert.strictEqual(lcConfig.error.is.MalformedXML, true); - assert.strictEqual(lcConfig.error.description, - "'StorageClass' must be one of 'a', 'b', but provided: aCrrLocation"); + assert.strictEqual( + lcConfig.error.description, + "'StorageClass' must be one of 'a', 'b', but provided: aCrrLocation", + ); + done(); + }); + }); + + it('should accept Expiration Days=0 (explicit bucket-emptying intent)', done => { + const xml = ` + + + empty-bucket + Enabled + + + 0 + + + `; + parseString(xml, (err, parsedXml) => { + assert.equal(err, null, 'Error parsing xml'); + const lcConfig = new LifecycleConfiguration(parsedXml, log, mockConfig).getLifecycleConfiguration(); + assert.strictEqual(lcConfig.error, undefined); + const action = lcConfig.rules[0].actions[0]; + assert.strictEqual(action.actionName, 'Expiration'); + assert.strictEqual(action.days, 0); + done(); + }); + }); + + it('should accept NoncurrentVersionExpiration NoncurrentDays=0', done => { + const xml = ` + + + empty-bucket + Enabled + + + 0 + + + `; + parseString(xml, (err, parsedXml) => { + assert.equal(err, null, 'Error parsing xml'); + const lcConfig = new LifecycleConfiguration(parsedXml, log, mockConfig).getLifecycleConfiguration(); + assert.strictEqual(lcConfig.error, undefined); + const action = lcConfig.rules[0].actions[0]; + assert.strictEqual(action.actionName, 'NoncurrentVersionExpiration'); + assert.strictEqual(action.days, 0); done(); }); }); + + it('should accept AbortIncompleteMultipartUpload DaysAfterInitiation=0', done => { + const xml = ` + + + empty-bucket + Enabled + + + 0 + + + `; + parseString(xml, (err, parsedXml) => { + assert.equal(err, null, 'Error parsing xml'); + const lcConfig = new LifecycleConfiguration(parsedXml, log, mockConfig).getLifecycleConfiguration(); + assert.strictEqual(lcConfig.error, undefined); + const action = lcConfig.rules[0].actions[0]; + assert.strictEqual(action.actionName, 'AbortIncompleteMultipartUpload'); + assert.strictEqual(action.days, 0); + done(); + }); + }); + + it('should round-trip Expiration Days=0 through getConfigXml', done => { + const xml = ` + + + empty-bucket + Enabled + + + 0 + + + `; + parseString(xml, (err, parsedXml) => { + assert.equal(err, null, 'Error parsing xml'); + const lcConfig = new LifecycleConfiguration(parsedXml, log, mockConfig).getLifecycleConfiguration(); + assert.strictEqual(lcConfig.error, undefined); + const outXml = LifecycleConfiguration.getConfigXml(lcConfig); + assert(outXml.includes('0'), `expected 0 in output: ${outXml}`); + done(); + }); + }); + + it('should log a warning when a 0-day action time is configured', done => { + const xml = ` + + + empty-bucket + Enabled + + + 0 + + + `; + parseString(xml, (err, parsedXml) => { + assert.equal(err, null, 'Error parsing xml'); + const auditLog = new DummyRequestLogger(); + new LifecycleConfiguration(parsedXml, auditLog, mockConfig).getLifecycleConfiguration(); + assert.strictEqual(auditLog.counts.warn, 1); + assert.deepStrictEqual(auditLog.ops[0], ['warn', ['lifecycle rule configured with a 0-day action time']]); + done(); + }); + }); + + it('should warn for NoncurrentVersionExpiration NoncurrentDays=0', done => { + const xml = ` + + + empty-noncurrent + Enabled + + + 0 + + + `; + parseString(xml, (err, parsedXml) => { + assert.equal(err, null, 'Error parsing xml'); + const auditLog = new DummyRequestLogger(); + new LifecycleConfiguration(parsedXml, auditLog, mockConfig).getLifecycleConfiguration(); + assert.strictEqual(auditLog.counts.warn, 1); + done(); + }); + }); + + it('should not warn for a non-deleting 0-day action (AbortIncompleteMultipartUpload)', done => { + const xml = ` + + + abort-now + Enabled + + + 0 + + + `; + parseString(xml, (err, parsedXml) => { + assert.equal(err, null, 'Error parsing xml'); + const auditLog = new DummyRequestLogger(); + const lcConfig = new LifecycleConfiguration(parsedXml, auditLog, mockConfig).getLifecycleConfiguration(); + assert.strictEqual(lcConfig.error, undefined); + assert.strictEqual(auditLog.counts.warn, 0); + done(); + }); + }); + + it('should not log for a positive day action time', done => { + const xml = ` + + + retain + Enabled + + + 1 + + + `; + parseString(xml, (err, parsedXml) => { + assert.equal(err, null, 'Error parsing xml'); + const auditLog = new DummyRequestLogger(); + new LifecycleConfiguration(parsedXml, auditLog, mockConfig).getLifecycleConfiguration(); + assert.strictEqual(auditLog.counts.warn, 0); + done(); + }); + }); + + ['Days', 'NoncurrentDays', 'DaysAfterInitiation'].forEach(field => { + const action = + field === 'Days' + ? 'Expiration' + : field === 'NoncurrentDays' + ? 'NoncurrentVersionExpiration' + : 'AbortIncompleteMultipartUpload'; + it(`should reject an empty <${field}> (not treat it as 0/NaN)`, done => { + const xml = ` + + + empty-day + Enabled + + <${action}> + <${field}> + + + `; + parseString(xml, (err, parsedXml) => { + assert.equal(err, null, 'Error parsing xml'); + const lcConfig = new LifecycleConfiguration(parsedXml, log, mockConfig).getLifecycleConfiguration(); + assert.notStrictEqual(lcConfig.error, undefined); + assert.strictEqual(lcConfig.error.description, `'${field}' in ${action} action must be an integer`); + done(); + }); + }); + }); }); describe('LifecycleConfiguration', () => { - const lifecycleConfiguration = new LifecycleConfiguration({}, mockConfig); + const lifecycleConfiguration = new LifecycleConfiguration({}, log, mockConfig); function getParsedXML() { return { LifecycleConfiguration: { - Rule: [{ - ID: ['test-id'], - Prefix: [''], - Status: ['Enabled'], - Expiration: [{ - Days: 1, - }], - }], + Rule: [ + { + ID: ['test-id'], + Prefix: [''], + Status: ['Enabled'], + Expiration: [ + { + Days: 1, + }, + ], + }, + ], }, }; } @@ -540,51 +825,63 @@ describe('LifecycleConfiguration', () => { it('should get Filter.And', () => { const rule = getParsedXML().LifecycleConfiguration.Rule[0]; delete rule.Prefix; - rule.Filter = [{ - And: [{ - Prefix: [''], - Tag: [{ - Key: ['a'], - Value: ['b'], - }, - { - Key: ['c'], - Value: ['d'], - }], - }], - }]; + rule.Filter = [ + { + And: [ + { + Prefix: [''], + Tag: [ + { + Key: ['a'], + Value: ['b'], + }, + { + Key: ['c'], + Value: ['d'], + }, + ], + }, + ], + }, + ]; const ruleFilter = lifecycleConfiguration._getRuleFilterDesc(rule); - assert.strictEqual(ruleFilter, 'filter ' + - "'(prefix= and tag: key=a, value=b and tag: key=c, value=d)'"); + assert.strictEqual(ruleFilter, 'filter ' + "'(prefix= and tag: key=a, value=b and tag: key=c, value=d)'"); }); it('should get Filter.And without Prefix', () => { const rule = getParsedXML().LifecycleConfiguration.Rule[0]; delete rule.Prefix; - rule.Filter = [{ - And: [{ - Tag: [{ - Key: ['a'], - Value: ['b'], - }, - { - Key: ['c'], - Value: ['d'], - }], - }], - }]; + rule.Filter = [ + { + And: [ + { + Tag: [ + { + Key: ['a'], + Value: ['b'], + }, + { + Key: ['c'], + Value: ['d'], + }, + ], + }, + ], + }, + ]; const ruleFilter = lifecycleConfiguration._getRuleFilterDesc(rule); - assert.strictEqual(ruleFilter, - "filter '(tag: key=a, value=b and tag: key=c, value=d)'"); + assert.strictEqual(ruleFilter, "filter '(tag: key=a, value=b and tag: key=c, value=d)'"); }); it('should get Filter with empty object', () => { const rule = { ID: ['test-id'], Status: ['Enabled'], - Expiration: [{ - Days: 1, - }], + Expiration: [ + { + Days: 1, + }, + ], }; rule.Filter = [{}]; const ruleFilter = lifecycleConfiguration._getRuleFilterDesc(rule); @@ -595,9 +892,11 @@ describe('LifecycleConfiguration', () => { const rule = { ID: ['test-id'], Status: ['Enabled'], - Expiration: [{ - Days: 1, - }], + Expiration: [ + { + Days: 1, + }, + ], }; rule.Filter = []; const ruleFilter = lifecycleConfiguration._getRuleFilterDesc(rule); @@ -670,8 +969,7 @@ describe('LifecycleConfiguration', () => { ancestor: 'b', rule: getParsedXML().LifecycleConfiguration.Rule[0], }); - const msg = "'StorageClass' must be different for 'b' actions " + - "in same 'Rule' with prefix ''"; + const msg = "'StorageClass' must be different for 'b' actions " + "in same 'Rule' with prefix ''"; expect(error.is.InvalidRequest).toBeTruthy(); expect(error.description).toEqual(msg); }); @@ -702,24 +1000,23 @@ describe('LifecycleConfiguration', () => { currentTimeType: 'Days', rule: getParsedXML().LifecycleConfiguration.Rule[0], }); - const msg = "Found mixed 'Date' and 'Days' based Transition " + - "actions in lifecycle rule for prefix ''"; + const msg = "Found mixed 'Date' and 'Days' based Transition " + "actions in lifecycle rule for prefix ''"; expect(error.is.InvalidRequest).toBeTruthy(); expect(error.description).toEqual(msg); }); - it('should return error when time type differs across expiration', - () => { - const error = lifecycleConfiguration._checkTimeType({ - usedTimeType: 'Date', - currentTimeType: 'Date', - rule: getParsedXML().LifecycleConfiguration.Rule[0], - }); - const msg = "Found mixed 'Date' and 'Days' based Expiration and " + - "Transition actions in lifecycle rule for prefix ''"; - expect(error.is.InvalidRequest).toBeTruthy(); - expect(error.description).toEqual(msg); + it('should return error when time type differs across expiration', () => { + const error = lifecycleConfiguration._checkTimeType({ + usedTimeType: 'Date', + currentTimeType: 'Date', + rule: getParsedXML().LifecycleConfiguration.Rule[0], }); + const msg = + "Found mixed 'Date' and 'Days' based Expiration and " + + "Transition actions in lifecycle rule for prefix ''"; + expect(error.is.InvalidRequest).toBeTruthy(); + expect(error.description).toEqual(msg); + }); }); describe('::_checkDate', () => { @@ -768,7 +1065,7 @@ describe('LifecycleConfiguration', () => { it('should return an error with a date that is not set to midnight', () => { const date = '2024-01-04T15:22:40Z'; const error = lifecycleConfiguration._checkDate(date); - const msg = '\'Date\' must be at midnight GMT'; + const msg = "'Date' must be at midnight GMT"; expect(error.is.InvalidArgument).toBeTruthy(); expect(error.description).toEqual(msg); }); @@ -776,7 +1073,7 @@ describe('LifecycleConfiguration', () => { it('should return an error with a date that is not set to midnight', () => { const date = '2024-01-08T00:00:00.123Z'; const error = lifecycleConfiguration._checkDate(date); - const msg = '\'Date\' must be at midnight GMT'; + const msg = "'Date' must be at midnight GMT"; expect(error.is.InvalidArgument).toBeTruthy(); expect(error.description).toEqual(msg); }); @@ -800,8 +1097,7 @@ describe('LifecycleConfiguration', () => { it('should return correctly parsed result object', () => { const rule = getRule(); - const result = - lifecycleConfiguration._parseNoncurrentVersionTransition(rule); + const result = lifecycleConfiguration._parseNoncurrentVersionTransition(rule); assert.deepStrictEqual(result, { nonCurrentVersionTransition: [ { @@ -819,10 +1115,8 @@ describe('LifecycleConfiguration', () => { it('should return parsed result object with error', () => { const rule = getRule(); rule.NoncurrentVersionTransition[0].NoncurrentDays[0] = '-1'; - const result = - lifecycleConfiguration._parseNoncurrentVersionTransition(rule); - const msg = "'NoncurrentDays' in NoncurrentVersionTransition " + - 'action must be nonnegative'; + const result = lifecycleConfiguration._parseNoncurrentVersionTransition(rule); + const msg = "'NoncurrentDays' in NoncurrentVersionTransition " + 'action must be nonnegative'; expect(result.error.is.InvalidArgument).toBeTruthy(); expect(result.error.description).toEqual(msg); }); @@ -861,8 +1155,7 @@ describe('LifecycleConfiguration', () => { }); }); - it('should return parsed result object with error when days is ' + - 'negative', () => { + it('should return parsed result object with error when days is ' + 'negative', () => { const rule = getRule(); rule.Transition[0].Days[0] = '-1'; const result = lifecycleConfiguration._parseTransition(rule); @@ -871,13 +1164,13 @@ describe('LifecycleConfiguration', () => { expect(result.error.description).toEqual(msg); }); - it('should return parsed result object with error when two ' + - 'transition days are the same', () => { + it('should return parsed result object with error when two ' + 'transition days are the same', () => { const rule = getRule(); rule.Prefix = ['prefix']; rule.Transition[1].Days[0] = '0'; const result = lifecycleConfiguration._parseTransition(rule); - const msg = "'Days' in the 'Transition' action for StorageClass " + + const msg = + "'Days' in the 'Transition' action for StorageClass " + "'a' for prefix 'prefix' must be at least one day apart from " + "prefix 'prefix' in the 'Transition' action for StorageClass " + "'b'"; @@ -887,8 +1180,7 @@ describe('LifecycleConfiguration', () => { }); describe('::_parseTransition with Date', () => { - it('should return parsed result object with error when dates are not ' + - 'more than one day apart', () => { + it('should return parsed result object with error when dates are not ' + 'more than one day apart', () => { const rule = { Prefix: ['prefix'], Transition: [ @@ -903,7 +1195,8 @@ describe('LifecycleConfiguration', () => { ], }; const result = lifecycleConfiguration._parseTransition(rule); - const msg = "'Date' in the 'Transition' action for StorageClass " + + const msg = + "'Date' in the 'Transition' action for StorageClass " + "'a' for prefix 'prefix' must be at least one day apart from " + "prefix 'prefix' in the 'Transition' action for StorageClass " + "'b'"; @@ -916,10 +1209,12 @@ describe('LifecycleConfiguration', () => { it('should not return error when only one transition', () => { const params = { rule: { - Transition: [{ - Days: ['0'], - StorageClass: ['a'], - }], + Transition: [ + { + Days: ['0'], + StorageClass: ['a'], + }, + ], }, days: 0, storageClass: 'a', @@ -928,17 +1223,19 @@ describe('LifecycleConfiguration', () => { assert.strictEqual(error, null); }); - it('should not return error when transitions have days greater than ' + - '24 hours apart', () => { + it('should not return error when transitions have days greater than ' + '24 hours apart', () => { const params = { rule: { - Transition: [{ - Days: ['0'], - StorageClass: ['a'], - }, { - Days: ['1'], - StorageClass: ['b'], - }], + Transition: [ + { + Days: ['0'], + StorageClass: ['a'], + }, + { + Days: ['1'], + StorageClass: ['b'], + }, + ], }, days: 0, storageClass: 'a', @@ -951,13 +1248,16 @@ describe('LifecycleConfiguration', () => { const params = { rule: { Prefix: 'prefix', - Transition: [{ - Days: ['0'], - StorageClass: ['a'], - }, { - Days: ['0'], - StorageClass: ['b'], - }], + Transition: [ + { + Days: ['0'], + StorageClass: ['a'], + }, + { + Days: ['0'], + StorageClass: ['b'], + }, + ], }, days: 0, storageClass: 'a', @@ -966,17 +1266,19 @@ describe('LifecycleConfiguration', () => { expect(error.is.InvalidArgument).toBeTruthy(); }); - it('should not return error when transitions have dates greater than ' + - '24 hours apart', () => { + it('should not return error when transitions have dates greater than ' + '24 hours apart', () => { const params = { rule: { - Transition: [{ - Date: ['2019-01-01T00:00:00.000Z'], - StorageClass: ['a'], - }, { - Date: ['2019-01-02T00:00:00.000Z'], - StorageClass: ['b'], - }], + Transition: [ + { + Date: ['2019-01-01T00:00:00.000Z'], + StorageClass: ['a'], + }, + { + Date: ['2019-01-02T00:00:00.000Z'], + StorageClass: ['b'], + }, + ], }, date: '2019-01-01T00:00:00.000Z', storageClass: 'a', @@ -985,18 +1287,20 @@ describe('LifecycleConfiguration', () => { assert.strictEqual(error, null); }); - it('should return error when transitions have dates less than 24 ' + - 'hours apart', () => { + it('should return error when transitions have dates less than 24 ' + 'hours apart', () => { const params = { rule: { Prefix: 'prefix', - Transition: [{ - Date: ['2019-01-01T00:00:00.000Z'], - StorageClass: ['a'], - }, { - Date: ['2019-01-01T23:59:59.999Z'], - StorageClass: ['b'], - }], + Transition: [ + { + Date: ['2019-01-01T00:00:00.000Z'], + StorageClass: ['a'], + }, + { + Date: ['2019-01-01T23:59:59.999Z'], + StorageClass: ['b'], + }, + ], }, date: '2019-01-01T00:00:00.000Z', storageClass: 'a', @@ -1016,9 +1320,7 @@ describe('LifecycleConfiguration::getConfigJson', () => { { ruleID: 'test-id', ruleStatus: 'Enabled', - actions: [ - { actionName: 'Expiration', days: 1 }, - ], + actions: [{ actionName: 'Expiration', days: 1 }], }, ], }, @@ -1040,9 +1342,7 @@ describe('LifecycleConfiguration::getConfigJson', () => { { ruleID: 'test-id', ruleStatus: 'Enabled', - actions: [ - { actionName: 'Expiration', days: 1 }, - ], + actions: [{ actionName: 'Expiration', days: 1 }], prefix: 'prefix', }, ], @@ -1065,9 +1365,7 @@ describe('LifecycleConfiguration::getConfigJson', () => { { ruleID: 'test-id', ruleStatus: 'Enabled', - actions: [ - { actionName: 'Expiration', days: 1 }, - ], + actions: [{ actionName: 'Expiration', days: 1 }], filter: { rulePrefix: 'prefix' }, }, ], @@ -1090,13 +1388,9 @@ describe('LifecycleConfiguration::getConfigJson', () => { { ruleID: 'test-id', ruleStatus: 'Enabled', - actions: [ - { actionName: 'Expiration', days: 1 }, - ], + actions: [{ actionName: 'Expiration', days: 1 }], filter: { - tags: [ - { key: 'key', val: 'val' }, - ], + tags: [{ key: 'key', val: 'val' }], }, prefix: 'prefix', }, @@ -1110,9 +1404,7 @@ describe('LifecycleConfiguration::getConfigJson', () => { Filter: { And: { Prefix: 'prefix', - Tags: [ - { Key: 'key', Value: 'val' }, - ], + Tags: [{ Key: 'key', Value: 'val' }], }, }, Expiration: { Days: 1 }, @@ -1127,14 +1419,10 @@ describe('LifecycleConfiguration::getConfigJson', () => { { ruleID: 'test-id', ruleStatus: 'Enabled', - actions: [ - { actionName: 'Expiration', days: 1 }, - ], + actions: [{ actionName: 'Expiration', days: 1 }], filter: { rulePrefix: 'prefix', - tags: [ - { key: 'key', val: 'val' }, - ], + tags: [{ key: 'key', val: 'val' }], }, }, ], @@ -1147,9 +1435,7 @@ describe('LifecycleConfiguration::getConfigJson', () => { Filter: { And: { Prefix: 'prefix', - Tags: [ - { Key: 'key', Value: 'val' }, - ], + Tags: [{ Key: 'key', Value: 'val' }], }, }, Expiration: { Days: 1 }, @@ -1164,9 +1450,7 @@ describe('LifecycleConfiguration::getConfigJson', () => { { ruleID: 'test-id', ruleStatus: 'Enabled', - actions: [ - { actionName: 'Expiration', days: 1 }, - ], + actions: [{ actionName: 'Expiration', days: 1 }], filter: { tags: [ { key: 'key1', val: 'val' }, @@ -1201,9 +1485,7 @@ describe('LifecycleConfiguration::getConfigJson', () => { { ruleID: 'test-id', ruleStatus: 'Enabled', - actions: [ - { actionName: 'Expiration', deleteMarker: 'true' }, - ], + actions: [{ actionName: 'Expiration', deleteMarker: 'true' }], prefix: '', }, ], @@ -1226,9 +1508,7 @@ describe('LifecycleConfiguration::getConfigJson', () => { { ruleID: 'test-id', ruleStatus: 'Enabled', - actions: [ - { actionName: 'Expiration', days: 10 }, - ], + actions: [{ actionName: 'Expiration', days: 10 }], prefix: '', }, ], @@ -1279,9 +1559,7 @@ describe('LifecycleConfiguration::getConfigJson', () => { { ruleID: 'test-id', ruleStatus: 'Enabled', - actions: [ - { actionName: 'NoncurrentVersionExpiration', days: 10 }, - ], + actions: [{ actionName: 'NoncurrentVersionExpiration', days: 10 }], prefix: '', }, ], @@ -1339,9 +1617,7 @@ describe('LifecycleConfiguration::getConfigJson', () => { { ruleID: 'test-id', ruleStatus: 'Enabled', - actions: [ - { actionName: 'AbortIncompleteMultipartUpload', days: 10 }, - ], + actions: [{ actionName: 'AbortIncompleteMultipartUpload', days: 10 }], prefix: '', }, ], @@ -1388,13 +1664,11 @@ describe('LifecycleConfiguration::getConfigJson', () => { ], ]; - tests.forEach(([msg, input, expected]) => it( - `should return correct configuration: ${msg}`, () => { - assert.deepStrictEqual( - LifecycleConfiguration.getConfigJson(input), - expected, - ); - })); + tests.forEach(([msg, input, expected]) => + it(`should return correct configuration: ${msg}`, () => { + assert.deepStrictEqual(LifecycleConfiguration.getConfigJson(input), expected); + }), + ); }); describe('LifecycleConfiguration.getConfigXml - XML escaping for special characters', () => { @@ -1403,15 +1677,19 @@ describe('LifecycleConfiguration.getConfigXml - XML escaping for special charact specialCharacters.forEach(char => { it(`should escape \`${char}\` in rule ID and generate valid XML`, done => { const config = { - rules: [{ - ruleID: `test-id${char}value`, - ruleStatus: 'Enabled', - prefix: 'logs/', - actions: [{ - actionName: 'Expiration', - days: 90, - }], - }], + rules: [ + { + ruleID: `test-id${char}value`, + ruleStatus: 'Enabled', + prefix: 'logs/', + actions: [ + { + actionName: 'Expiration', + days: 90, + }, + ], + }, + ], }; const xml = LifecycleConfiguration.getConfigXml(config); @@ -1426,15 +1704,19 @@ describe('LifecycleConfiguration.getConfigXml - XML escaping for special charact it(`should escape \`${char}\` in prefix`, done => { const config = { - rules: [{ - ruleID: 'test-id', - ruleStatus: 'Enabled', - prefix: `logs/${char}path/`, - actions: [{ - actionName: 'Expiration', - days: 90, - }], - }], + rules: [ + { + ruleID: 'test-id', + ruleStatus: 'Enabled', + prefix: `logs/${char}path/`, + actions: [ + { + actionName: 'Expiration', + days: 90, + }, + ], + }, + ], }; const xml = LifecycleConfiguration.getConfigXml(config); @@ -1449,20 +1731,26 @@ describe('LifecycleConfiguration.getConfigXml - XML escaping for special charact it(`should escape \`${char}\` in tag key and value`, done => { const config = { - rules: [{ - ruleID: 'test-id', - ruleStatus: 'Enabled', - filter: { - tags: [{ - key: `env${char}key`, - val: `value${char}data`, - }], + rules: [ + { + ruleID: 'test-id', + ruleStatus: 'Enabled', + filter: { + tags: [ + { + key: `env${char}key`, + val: `value${char}data`, + }, + ], + }, + actions: [ + { + actionName: 'Expiration', + days: 90, + }, + ], }, - actions: [{ - actionName: 'Expiration', - days: 90, - }], - }], + ], }; const xml = LifecycleConfiguration.getConfigXml(config); diff --git a/tests/unit/s3middleware/LifecycleUtils.spec.js b/tests/unit/s3middleware/LifecycleUtils.spec.js index a397589ad..772dd81be 100644 --- a/tests/unit/s3middleware/LifecycleUtils.spec.js +++ b/tests/unit/s3middleware/LifecycleUtils.spec.js @@ -51,13 +51,10 @@ describe('LifecycleUtils::getApplicableRules', () => { it('should return earliest applicable expirations', () => { const filteredRules = [ - new LifecycleRule().addID('task-1').addExpiration('Date', FUTURE) - .build(), + new LifecycleRule().addID('task-1').addExpiration('Date', FUTURE).build(), new LifecycleRule().addID('task-2').addExpiration('Days', 10).build(), - new LifecycleRule().addID('task-3').addExpiration('Date', PAST) - .build(), - new LifecycleRule().addID('task-4').addExpiration('Date', CURRENT) - .build(), + new LifecycleRule().addID('task-3').addExpiration('Date', PAST).build(), + new LifecycleRule().addID('task-4').addExpiration('Date', CURRENT).build(), new LifecycleRule().addID('task-5').addExpiration('Days', 5).build(), ]; @@ -71,17 +68,39 @@ describe('LifecycleUtils::getApplicableRules', () => { assert.strictEqual(res2.Expiration.Date, CURRENT); }); + it('should select Expiration Days=0 and prefer it over a larger Days', () => { + const filteredRules = [ + new LifecycleRule().addID('task-1').addExpiration('Days', 10).build(), + new LifecycleRule().addID('task-2').addExpiration('Days', 0).build(), + ]; + + const res = lutils.getApplicableRules(filteredRules); + assert.strictEqual(res.Expiration.Days, 0); + assert.strictEqual(res.Expiration.ID, 'task-2'); + }); + + it('should select NoncurrentVersionExpiration NoncurrentDays=0', () => { + const filteredRules = [new LifecycleRule().addID('task-1').addNCVExpiration('NoncurrentDays', 0).build()]; + + const res = lutils.getApplicableRules(filteredRules); + assert.strictEqual(res.NoncurrentVersionExpiration.NoncurrentDays, 0); + }); + + it('should select AbortIncompleteMultipartUpload DaysAfterInitiation=0', () => { + const filteredRules = [new LifecycleRule().addID('task-1').addAbortMPU(0).build()]; + + const res = lutils.getApplicableRules(filteredRules); + assert.strictEqual(res.AbortIncompleteMultipartUpload.DaysAfterInitiation, 0); + }); + it('should return earliest applicable rules', () => { const filteredRules = [ - new LifecycleRule().addID('task-1').addExpiration('Date', FUTURE) - .build(), + new LifecycleRule().addID('task-1').addExpiration('Date', FUTURE).build(), new LifecycleRule().addID('task-2').addAbortMPU(18).build(), - new LifecycleRule().addID('task-3').addExpiration('Date', PAST) - .build(), + new LifecycleRule().addID('task-3').addExpiration('Date', PAST).build(), new LifecycleRule().addID('task-4').addNCVExpiration('NoncurrentDays', 3).build(), new LifecycleRule().addID('task-5').addNCVExpiration('NoncurrentDays', 12).build(), - new LifecycleRule().addID('task-6').addExpiration('Date', CURRENT) - .build(), + new LifecycleRule().addID('task-6').addExpiration('Date', CURRENT).build(), new LifecycleRule().addID('task-7').addNCVExpiration(7).build(), new LifecycleRule().addID('task-8').addAbortMPU(4).build(), new LifecycleRule().addID('task-9').addAbortMPU(22).build(), @@ -90,27 +109,26 @@ describe('LifecycleUtils::getApplicableRules', () => { const res = lutils.getApplicableRules(filteredRules); assert.deepStrictEqual(Object.keys(res.Expiration), ['ID', 'Date']); assert.deepStrictEqual(res.Expiration, { ID: 'task-3', Date: PAST }); - assert.strictEqual( - res.AbortIncompleteMultipartUpload.DaysAfterInitiation, 4); - assert.strictEqual( - res.NoncurrentVersionExpiration.NoncurrentDays, 3); + assert.strictEqual(res.AbortIncompleteMultipartUpload.DaysAfterInitiation, 4); + assert.strictEqual(res.NoncurrentVersionExpiration.NoncurrentDays, 3); }); it('should return earliest applicable rules with NewerNoncurrentVersions', () => { const filteredRules = [ - new LifecycleRule().addID('task-1').addExpiration('Date', FUTURE) - .build(), + new LifecycleRule().addID('task-1').addExpiration('Date', FUTURE).build(), new LifecycleRule().addID('task-2').addAbortMPU(18).build(), - new LifecycleRule().addID('task-3').addExpiration('Date', PAST) - .build(), - new LifecycleRule().addID('task-4') + new LifecycleRule().addID('task-3').addExpiration('Date', PAST).build(), + new LifecycleRule() + .addID('task-4') .addNCVExpiration('NoncurrentDays', 3) - .addNCVExpiration('NewerNoncurrentVersions', 5).build(), - new LifecycleRule().addID('task-5') + .addNCVExpiration('NewerNoncurrentVersions', 5) + .build(), + new LifecycleRule() + .addID('task-5') .addNCVExpiration('NoncurrentDays', 12) - .addNCVExpiration('NewerNoncurrentVersions', 10).build(), - new LifecycleRule().addID('task-6').addExpiration('Date', CURRENT) + .addNCVExpiration('NewerNoncurrentVersions', 10) .build(), + new LifecycleRule().addID('task-6').addExpiration('Date', CURRENT).build(), new LifecycleRule().addID('task-7').addNCVExpiration(7).build(), new LifecycleRule().addID('task-8').addAbortMPU(4).build(), new LifecycleRule().addID('task-9').addAbortMPU(22).build(), @@ -119,12 +137,9 @@ describe('LifecycleUtils::getApplicableRules', () => { const res = lutils.getApplicableRules(filteredRules); assert.deepStrictEqual(Object.keys(res.Expiration), ['ID', 'Date']); assert.deepStrictEqual(res.Expiration, { ID: 'task-3', Date: PAST }); - assert.strictEqual( - res.AbortIncompleteMultipartUpload.DaysAfterInitiation, 4); - assert.strictEqual( - res.NoncurrentVersionExpiration.NoncurrentDays, 3); - assert.strictEqual( - res.NoncurrentVersionExpiration.NewerNoncurrentVersions, 5); + assert.strictEqual(res.AbortIncompleteMultipartUpload.DaysAfterInitiation, 4); + assert.strictEqual(res.NoncurrentVersionExpiration.NoncurrentDays, 3); + assert.strictEqual(res.NoncurrentVersionExpiration.NewerNoncurrentVersions, 5); }); it('should return Transition with Days', () => { @@ -178,10 +193,12 @@ describe('LifecycleUtils::getApplicableRules', () => { it('should return Transition with Date', () => { const applicableRules = [ new LifecycleRule() - .addTransitions([{ - Date: 0, - StorageClass: 'zenko', - }]) + .addTransitions([ + { + Date: 0, + StorageClass: 'zenko', + }, + ]) .build(), ]; const lastModified = getDate({ numberOfDaysFromNow: -1 }); @@ -198,16 +215,20 @@ describe('LifecycleUtils::getApplicableRules', () => { it('should return Transition across many rules: first rule', () => { const applicableRules = [ new LifecycleRule() - .addTransitions([{ - Days: 1, - StorageClass: 'zenko-1', - }]) + .addTransitions([ + { + Days: 1, + StorageClass: 'zenko-1', + }, + ]) .build(), new LifecycleRule() - .addTransitions([{ - Days: 3, - StorageClass: 'zenko-3', - }]) + .addTransitions([ + { + Days: 3, + StorageClass: 'zenko-3', + }, + ]) .build(), ]; const lastModified = getDate({ numberOfDaysFromNow: -2 }); @@ -224,16 +245,20 @@ describe('LifecycleUtils::getApplicableRules', () => { it('should return Transition across many rules: second rule', () => { const applicableRules = [ new LifecycleRule() - .addTransitions([{ - Days: 1, - StorageClass: 'zenko-1', - }]) + .addTransitions([ + { + Days: 1, + StorageClass: 'zenko-1', + }, + ]) .build(), new LifecycleRule() - .addTransitions([{ - Days: 3, - StorageClass: 'zenko-3', - }]) + .addTransitions([ + { + Days: 3, + StorageClass: 'zenko-3', + }, + ]) .build(), ]; const lastModified = getDate({ numberOfDaysFromNow: -4 }); @@ -247,23 +272,27 @@ describe('LifecycleUtils::getApplicableRules', () => { }); }); - it('should return Transition across many rules: first rule with ' + - 'multiple transitions', () => { + it('should return Transition across many rules: first rule with ' + 'multiple transitions', () => { const applicableRules = [ new LifecycleRule() - .addTransitions([{ - Days: 1, - StorageClass: 'zenko-1', - }, { - Days: 3, - StorageClass: 'zenko-3', - }]) + .addTransitions([ + { + Days: 1, + StorageClass: 'zenko-1', + }, + { + Days: 3, + StorageClass: 'zenko-3', + }, + ]) .build(), new LifecycleRule() - .addTransitions([{ - Days: 4, - StorageClass: 'zenko-4', - }]) + .addTransitions([ + { + Days: 4, + StorageClass: 'zenko-4', + }, + ]) .build(), ]; const lastModified = getDate({ numberOfDaysFromNow: -2 }); @@ -277,26 +306,31 @@ describe('LifecycleUtils::getApplicableRules', () => { }); }); - it('should return Transition across many rules: second rule with ' + - 'multiple transitions', () => { + it('should return Transition across many rules: second rule with ' + 'multiple transitions', () => { const applicableRules = [ new LifecycleRule() - .addTransitions([{ - Days: 1, - StorageClass: 'zenko-1', - }, { - Days: 3, - StorageClass: 'zenko-3', - }]) + .addTransitions([ + { + Days: 1, + StorageClass: 'zenko-1', + }, + { + Days: 3, + StorageClass: 'zenko-3', + }, + ]) .build(), new LifecycleRule() - .addTransitions([{ - Days: 4, - StorageClass: 'zenko-4', - }, { - Days: 6, - StorageClass: 'zenko-6', - }]) + .addTransitions([ + { + Days: 4, + StorageClass: 'zenko-4', + }, + { + Days: 6, + StorageClass: 'zenko-6', + }, + ]) .build(), ]; const lastModified = getDate({ numberOfDaysFromNow: -5 }); @@ -310,21 +344,24 @@ describe('LifecycleUtils::getApplicableRules', () => { }); }); - it('should return Transition across many rules: combination of Date ' + - 'and Days gets Date result', () => { + it('should return Transition across many rules: combination of Date ' + 'and Days gets Date result', () => { const applicableDate = getDate({ numberOfDaysFromNow: -1 }); const applicableRules = [ new LifecycleRule() - .addTransitions([{ - Days: 1, - StorageClass: 'zenko-1', - }]) + .addTransitions([ + { + Days: 1, + StorageClass: 'zenko-1', + }, + ]) .build(), new LifecycleRule() - .addTransitions([{ - Date: applicableDate, - StorageClass: 'zenko-3', - }]) + .addTransitions([ + { + Date: applicableDate, + StorageClass: 'zenko-3', + }, + ]) .build(), ]; const lastModified = getDate({ numberOfDaysFromNow: -4 }); @@ -338,20 +375,23 @@ describe('LifecycleUtils::getApplicableRules', () => { }); }); - it('should return Transition across many rules: combination of Date ' + - 'and Days gets Days result', () => { + it('should return Transition across many rules: combination of Date ' + 'and Days gets Days result', () => { const applicableRules = [ new LifecycleRule() - .addTransitions([{ - Days: 3, - StorageClass: 'zenko-1', - }]) + .addTransitions([ + { + Days: 3, + StorageClass: 'zenko-1', + }, + ]) .build(), new LifecycleRule() - .addTransitions([{ - Date: getDate({ numberOfDaysFromNow: -4 }), - StorageClass: 'zenko-3', - }]) + .addTransitions([ + { + Date: getDate({ numberOfDaysFromNow: -4 }), + StorageClass: 'zenko-3', + }, + ]) .build(), ]; const lastModified = getDate({ numberOfDaysFromNow: -4 }); @@ -365,8 +405,7 @@ describe('LifecycleUtils::getApplicableRules', () => { }); }); - it('should not return transition when Transitions has no applicable ' + - 'rule: Days', () => { + it('should not return transition when Transitions has no applicable ' + 'rule: Days', () => { const applicableRules = [ new LifecycleRule() .addTransitions([ @@ -383,14 +422,15 @@ describe('LifecycleUtils::getApplicableRules', () => { assert.strictEqual(rules.Transition, undefined); }); - it('should not return transition when Transitions has no applicable ' + - 'rule: Date', () => { + it('should not return transition when Transitions has no applicable ' + 'rule: Date', () => { const applicableRules = [ new LifecycleRule() - .addTransitions([{ - Date: new Date(getDate({ numberOfDaysFromNow: 1 })), - StorageClass: 'zenko', - }]) + .addTransitions([ + { + Date: new Date(getDate({ numberOfDaysFromNow: 1 })), + StorageClass: 'zenko', + }, + ]) .build(), ]; const lastModified = getDate({ numberOfDaysFromNow: 0 }); @@ -399,23 +439,14 @@ describe('LifecycleUtils::getApplicableRules', () => { assert.strictEqual(rules.Transition, undefined); }); - it('should not return transition when Transitions is an empty ' + - 'array', () => { - const applicableRules = [ - new LifecycleRule() - .addTransitions([]) - .build(), - ]; + it('should not return transition when Transitions is an empty ' + 'array', () => { + const applicableRules = [new LifecycleRule().addTransitions([]).build()]; const rules = lutils.getApplicableRules(applicableRules, {}); assert.strictEqual(rules.Transition, undefined); }); it('should not return transition when Transitions is undefined', () => { - const applicableRules = [ - new LifecycleRule() - .addExpiration('Days', 1) - .build(), - ]; + const applicableRules = [new LifecycleRule().addExpiration('Days', 1).build()]; const rules = lutils.getApplicableRules(applicableRules, {}); assert.strictEqual(rules.Transition, undefined); }); @@ -471,16 +502,20 @@ describe('LifecycleUtils::getApplicableRules', () => { it('should return Transition across many rules: first rule', () => { const applicableRules = [ new LifecycleRule() - .addNCVTransitions([{ - NoncurrentDays: 1, - StorageClass: 'zenko-1', - }]) + .addNCVTransitions([ + { + NoncurrentDays: 1, + StorageClass: 'zenko-1', + }, + ]) .build(), new LifecycleRule() - .addNCVTransitions([{ - NoncurrentDays: 3, - StorageClass: 'zenko-3', - }]) + .addNCVTransitions([ + { + NoncurrentDays: 3, + StorageClass: 'zenko-3', + }, + ]) .build(), ]; const lastModified = getDate({ numberOfDaysFromNow: -2 }); @@ -497,16 +532,20 @@ describe('LifecycleUtils::getApplicableRules', () => { it('should return Transition across many rules: second rule', () => { const applicableRules = [ new LifecycleRule() - .addNCVTransitions([{ - NoncurrentDays: 1, - StorageClass: 'zenko-1', - }]) + .addNCVTransitions([ + { + NoncurrentDays: 1, + StorageClass: 'zenko-1', + }, + ]) .build(), new LifecycleRule() - .addNCVTransitions([{ - NoncurrentDays: 3, - StorageClass: 'zenko-3', - }]) + .addNCVTransitions([ + { + NoncurrentDays: 3, + StorageClass: 'zenko-3', + }, + ]) .build(), ]; const lastModified = getDate({ numberOfDaysFromNow: -4 }); @@ -520,23 +559,27 @@ describe('LifecycleUtils::getApplicableRules', () => { }); }); - it('should return Transition across many rules: first rule with ' + - 'multiple transitions', () => { + it('should return Transition across many rules: first rule with ' + 'multiple transitions', () => { const applicableRules = [ new LifecycleRule() - .addNCVTransitions([{ - NoncurrentDays: 1, - StorageClass: 'zenko-1', - }, { - NoncurrentDays: 3, - StorageClass: 'zenko-3', - }]) + .addNCVTransitions([ + { + NoncurrentDays: 1, + StorageClass: 'zenko-1', + }, + { + NoncurrentDays: 3, + StorageClass: 'zenko-3', + }, + ]) .build(), new LifecycleRule() - .addNCVTransitions([{ - NoncurrentDays: 4, - StorageClass: 'zenko-4', - }]) + .addNCVTransitions([ + { + NoncurrentDays: 4, + StorageClass: 'zenko-4', + }, + ]) .build(), ]; const lastModified = getDate({ numberOfDaysFromNow: -2 }); @@ -550,26 +593,31 @@ describe('LifecycleUtils::getApplicableRules', () => { }); }); - it('should return Transition across many rules: second rule with ' + - 'multiple transitions', () => { + it('should return Transition across many rules: second rule with ' + 'multiple transitions', () => { const applicableRules = [ new LifecycleRule() - .addNCVTransitions([{ - NoncurrentDays: 1, - StorageClass: 'zenko-1', - }, { - NoncurrentDays: 3, - StorageClass: 'zenko-3', - }]) + .addNCVTransitions([ + { + NoncurrentDays: 1, + StorageClass: 'zenko-1', + }, + { + NoncurrentDays: 3, + StorageClass: 'zenko-3', + }, + ]) .build(), new LifecycleRule() - .addNCVTransitions([{ - NoncurrentDays: 4, - StorageClass: 'zenko-4', - }, { - NoncurrentDays: 6, - StorageClass: 'zenko-6', - }]) + .addNCVTransitions([ + { + NoncurrentDays: 4, + StorageClass: 'zenko-4', + }, + { + NoncurrentDays: 6, + StorageClass: 'zenko-6', + }, + ]) .build(), ]; const lastModified = getDate({ numberOfDaysFromNow: -5 }); @@ -583,8 +631,7 @@ describe('LifecycleUtils::getApplicableRules', () => { }); }); - it('should not return transition when Transitions has no applicable ' + - 'rule: Days', () => { + it('should not return transition when Transitions has no applicable ' + 'rule: Days', () => { const applicableRules = [ new LifecycleRule() .addNCVTransitions([ @@ -601,30 +648,20 @@ describe('LifecycleUtils::getApplicableRules', () => { assert.strictEqual(rules.Transition, undefined); }); - it('should not return transition when Transitions is an empty ' + - 'array', () => { - const applicableRules = [ - new LifecycleRule() - .addNCVTransitions([]) - .build(), - ]; + it('should not return transition when Transitions is an empty ' + 'array', () => { + const applicableRules = [new LifecycleRule().addNCVTransitions([]).build()]; const rules = lutils.getApplicableRules(applicableRules, {}); assert.strictEqual(rules.Transition, undefined); }); it('should not return noncurrentTransition when undefined', () => { - const applicableRules = [ - new LifecycleRule() - .addExpiration('Days', 1) - .build(), - ]; + const applicableRules = [new LifecycleRule().addExpiration('Days', 1).build()]; const rules = lutils.getApplicableRules(applicableRules, {}); assert.strictEqual(rules.Transition, undefined); }); describe('transitioning to the same storage class', () => { - it('should not return transition when applicable transition is ' + - 'already stored at the destination', () => { + it('should not return transition when applicable transition is ' + 'already stored at the destination', () => { const applicableRules = [ new LifecycleRule() .addTransitions([ @@ -641,34 +678,36 @@ describe('LifecycleUtils::getApplicableRules', () => { assert.strictEqual(rules.Transition, undefined); }); - it('should not return transition when applicable transition is ' + - 'already stored at the destination: multiple rules', () => { - const applicableRules = [ - new LifecycleRule() - .addTransitions([ - { - Days: 2, - StorageClass: 'zenko', - }, - ]) - .build(), - new LifecycleRule() - .addTransitions([ - { - Days: 1, - StorageClass: 'STANDARD', - }, - ]) - .build(), - ]; - const lastModified = getDate({ numberOfDaysFromNow: -3 }); - const object = getMetadataObject(lastModified, 'zenko'); - const rules = lutils.getApplicableRules(applicableRules, object); - assert.strictEqual(rules.Transition, undefined); - }); + it( + 'should not return transition when applicable transition is ' + + 'already stored at the destination: multiple rules', + () => { + const applicableRules = [ + new LifecycleRule() + .addTransitions([ + { + Days: 2, + StorageClass: 'zenko', + }, + ]) + .build(), + new LifecycleRule() + .addTransitions([ + { + Days: 1, + StorageClass: 'STANDARD', + }, + ]) + .build(), + ]; + const lastModified = getDate({ numberOfDaysFromNow: -3 }); + const object = getMetadataObject(lastModified, 'zenko'); + const rules = lutils.getApplicableRules(applicableRules, object); + assert.strictEqual(rules.Transition, undefined); + }, + ); - it('should not return transition when applicable transition is ' + - 'already stored at the destination', () => { + it('should not return transition when applicable transition is ' + 'already stored at the destination', () => { const applicableRules = [ new LifecycleRule() .addNCVTransitions([ @@ -685,31 +724,34 @@ describe('LifecycleUtils::getApplicableRules', () => { assert.strictEqual(rules.Transition, undefined); }); - it('should not return transition when applicable transition is ' + - 'already stored at the destination: multiple rules', () => { - const applicableRules = [ - new LifecycleRule() - .addNCVTransitions([ - { - Days: 2, - StorageClass: 'zenko', - }, - ]) - .build(), - new LifecycleRule() - .addNCVTransitions([ - { - Days: 1, - StorageClass: 'STANDARD', - }, - ]) - .build(), - ]; - const lastModified = getDate({ numberOfDaysFromNow: -3 }); - const object = getMetadataObject(lastModified, 'zenko'); - const rules = lutils.getApplicableRules(applicableRules, object); - assert.strictEqual(rules.Transition, undefined); - }); + it( + 'should not return transition when applicable transition is ' + + 'already stored at the destination: multiple rules', + () => { + const applicableRules = [ + new LifecycleRule() + .addNCVTransitions([ + { + Days: 2, + StorageClass: 'zenko', + }, + ]) + .build(), + new LifecycleRule() + .addNCVTransitions([ + { + Days: 1, + StorageClass: 'STANDARD', + }, + ]) + .build(), + ]; + const lastModified = getDate({ numberOfDaysFromNow: -3 }); + const object = getMetadataObject(lastModified, 'zenko'); + const rules = lutils.getApplicableRules(applicableRules, object); + assert.strictEqual(rules.Transition, undefined); + }, + ); }); }); @@ -735,8 +777,7 @@ describe('LifecycleUtils::filterRules', () => { const objTags = { TagSet: [] }; const res = lutils.filterRules(mBucketRules, item, objTags); - const expected = mBucketRules.filter(rule => - rule.Status === 'Enabled'); + const expected = mBucketRules.filter(rule => rule.Status === 'Enabled'); assert.deepStrictEqual(getRuleIDs(res), getRuleIDs(expected)); }); @@ -763,36 +804,40 @@ describe('LifecycleUtils::filterRules', () => { const res1 = lutils.filterRules(mBucketRules, item1, objTags); assert.strictEqual(res1.length, 3); - const expRes1 = getRuleIDs(mBucketRules.filter(rule => { - if (!rule.Filter || !rule.Filter.Prefix) { - return true; - } - if (item1.Key.startsWith(rule.Filter.Prefix)) { - return true; - } - return false; - })); + const expRes1 = getRuleIDs( + mBucketRules.filter(rule => { + if (!rule.Filter || !rule.Filter.Prefix) { + return true; + } + if (item1.Key.startsWith(rule.Filter.Prefix)) { + return true; + } + return false; + }), + ); assert.deepStrictEqual(expRes1, getRuleIDs(res1)); const res2 = lutils.filterRules(mBucketRules, item2, objTags); assert.strictEqual(res2.length, 2); - const expRes2 = getRuleIDs(mBucketRules.filter(rule => - (rule.Filter && rule.Filter.Prefix && rule.Filter.Prefix.startsWith('cat-')) - || (!rule.Filter || !rule.Filter.Prefix))); + const expRes2 = getRuleIDs( + mBucketRules.filter( + rule => + (rule.Filter && rule.Filter.Prefix && rule.Filter.Prefix.startsWith('cat-')) || + !rule.Filter || + !rule.Filter.Prefix, + ), + ); assert.deepStrictEqual(expRes2, getRuleIDs(res2)); }); it('should filter out unmatched single tags', () => { const mBucketRules = [ new LifecycleRule().addID('task-1').addTag('tag1', 'val1').build(), - new LifecycleRule().addID('task-2').addTag('tag3-1', 'val3') - .addTag('tag3-2', 'val3').build(), + new LifecycleRule().addID('task-2').addTag('tag3-1', 'val3').addTag('tag3-2', 'val3').build(), new LifecycleRule().addID('task-3').addTag('tag3-1', 'val3').build(), new LifecycleRule().addID('task-4').addTag('tag1', 'val1').build(), - new LifecycleRule().addID('task-5').addTag('tag3-2', 'val3') - .addTag('tag3-1', 'false').build(), - new LifecycleRule().addID('task-6').addTag('tag3-2', 'val3') - .addTag('tag3-1', 'val3').build(), + new LifecycleRule().addID('task-5').addTag('tag3-2', 'val3').addTag('tag3-1', 'false').build(), + new LifecycleRule().addID('task-6').addTag('tag3-2', 'val3').addTag('tag3-1', 'val3').build(), ]; const item = { Key: 'example-item', @@ -801,87 +846,105 @@ describe('LifecycleUtils::filterRules', () => { const objTags1 = { TagSet: [{ Key: 'tag1', Value: 'val1' }] }; const res1 = lutils.filterRules(mBucketRules, item, objTags1); assert.strictEqual(res1.length, 2); - const expRes1 = getRuleIDs(mBucketRules.filter(rule => - (rule.Filter && rule.Filter.Tag && - rule.Filter.Tag.Key === 'tag1' && - rule.Filter.Tag.Value === 'val1'), - )); + const expRes1 = getRuleIDs( + mBucketRules.filter( + rule => + rule.Filter && + rule.Filter.Tag && + rule.Filter.Tag.Key === 'tag1' && + rule.Filter.Tag.Value === 'val1', + ), + ); assert.deepStrictEqual(expRes1, getRuleIDs(res1)); const objTags2 = { TagSet: [{ Key: 'tag3-1', Value: 'val3' }] }; const res2 = lutils.filterRules(mBucketRules, item, objTags2); assert.strictEqual(res2.length, 1); - const expRes2 = getRuleIDs(mBucketRules.filter(rule => - rule.Filter && rule.Filter.Tag && - rule.Filter.Tag.Key === 'tag3-1' && - rule.Filter.Tag.Value === 'val3', - )); + const expRes2 = getRuleIDs( + mBucketRules.filter( + rule => + rule.Filter && + rule.Filter.Tag && + rule.Filter.Tag.Key === 'tag3-1' && + rule.Filter.Tag.Value === 'val3', + ), + ); assert.deepStrictEqual(expRes2, getRuleIDs(res2)); }); it('should filter out unmatched multiple tags', () => { const mBucketRules = [ - new LifecycleRule().addID('task-1').addTag('tag1', 'val1') - .addTag('tag2', 'val1').build(), + new LifecycleRule().addID('task-1').addTag('tag1', 'val1').addTag('tag2', 'val1').build(), new LifecycleRule().addID('task-2').addTag('tag1', 'val1').build(), new LifecycleRule().addID('task-3').addTag('tag2', 'val1').build(), new LifecycleRule().addID('task-4').addTag('tag2', 'false').build(), - new LifecycleRule().addID('task-5').addTag('tag2', 'val1') - .addTag('tag1', 'false').build(), - new LifecycleRule().addID('task-6').addTag('tag2', 'val1') - .addTag('tag1', 'val1').build(), - new LifecycleRule().addID('task-7').addTag('tag2', 'val1') - .addTag('tag1', 'val1').addTag('tag3', 'val1').build(), - new LifecycleRule().addID('task-8').addTag('tag2', 'val1') - .addTag('tag1', 'val1').addTag('tag3', 'false').build(), + new LifecycleRule().addID('task-5').addTag('tag2', 'val1').addTag('tag1', 'false').build(), + new LifecycleRule().addID('task-6').addTag('tag2', 'val1').addTag('tag1', 'val1').build(), + new LifecycleRule() + .addID('task-7') + .addTag('tag2', 'val1') + .addTag('tag1', 'val1') + .addTag('tag3', 'val1') + .build(), + new LifecycleRule() + .addID('task-8') + .addTag('tag2', 'val1') + .addTag('tag1', 'val1') + .addTag('tag3', 'false') + .build(), new LifecycleRule().addID('task-9').build(), ]; const item = { Key: 'example-item', LastModified: CURRENT, }; - const objTags1 = { TagSet: [ - { Key: 'tag1', Value: 'val1' }, - { Key: 'tag2', Value: 'val1' }, - ] }; + const objTags1 = { + TagSet: [ + { Key: 'tag1', Value: 'val1' }, + { Key: 'tag2', Value: 'val1' }, + ], + }; const res1 = lutils.filterRules(mBucketRules, item, objTags1); assert.strictEqual(res1.length, 5); - assert.deepStrictEqual(getRuleIDs(res1), ['task-1', 'task-2', - 'task-3', 'task-6', 'task-9']); + assert.deepStrictEqual(getRuleIDs(res1), ['task-1', 'task-2', 'task-3', 'task-6', 'task-9']); - const objTags2 = { TagSet: [ - { Key: 'tag2', Value: 'val1' }, - ] }; + const objTags2 = { TagSet: [{ Key: 'tag2', Value: 'val1' }] }; const res2 = lutils.filterRules(mBucketRules, item, objTags2); assert.strictEqual(res2.length, 2); assert.deepStrictEqual(getRuleIDs(res2), ['task-3', 'task-9']); - const objTags3 = { TagSet: [ - { Key: 'tag2', Value: 'val1' }, - { Key: 'tag1', Value: 'val1' }, - { Key: 'tag3', Value: 'val1' }, - ] }; + const objTags3 = { + TagSet: [ + { Key: 'tag2', Value: 'val1' }, + { Key: 'tag1', Value: 'val1' }, + { Key: 'tag3', Value: 'val1' }, + ], + }; const res3 = lutils.filterRules(mBucketRules, item, objTags3); assert.strictEqual(res3.length, 6); - assert.deepStrictEqual(getRuleIDs(res3), ['task-1', 'task-2', - 'task-3', 'task-6', 'task-7', 'task-9']); + assert.deepStrictEqual(getRuleIDs(res3), ['task-1', 'task-2', 'task-3', 'task-6', 'task-7', 'task-9']); }); it('should filter correctly for an object with no tags', () => { const mBucketRules = [ - new LifecycleRule().addID('task-1').addTag('tag1', 'val1') - .addTag('tag2', 'val1').build(), + new LifecycleRule().addID('task-1').addTag('tag1', 'val1').addTag('tag2', 'val1').build(), new LifecycleRule().addID('task-2').addTag('tag1', 'val1').build(), new LifecycleRule().addID('task-3').addTag('tag2', 'val1').build(), new LifecycleRule().addID('task-4').addTag('tag2', 'false').build(), - new LifecycleRule().addID('task-5').addTag('tag2', 'val1') - .addTag('tag1', 'false').build(), - new LifecycleRule().addID('task-6').addTag('tag2', 'val1') - .addTag('tag1', 'val1').build(), - new LifecycleRule().addID('task-7').addTag('tag2', 'val1') - .addTag('tag1', 'val1').addTag('tag3', 'val1').build(), - new LifecycleRule().addID('task-8').addTag('tag2', 'val1') - .addTag('tag1', 'val1').addTag('tag3', 'false').build(), + new LifecycleRule().addID('task-5').addTag('tag2', 'val1').addTag('tag1', 'false').build(), + new LifecycleRule().addID('task-6').addTag('tag2', 'val1').addTag('tag1', 'val1').build(), + new LifecycleRule() + .addID('task-7') + .addTag('tag2', 'val1') + .addTag('tag1', 'val1') + .addTag('tag3', 'val1') + .build(), + new LifecycleRule() + .addID('task-8') + .addTag('tag2', 'val1') + .addTag('tag1', 'val1') + .addTag('tag3', 'false') + .build(), new LifecycleRule().addID('task-9').build(), ]; const item = { @@ -1183,7 +1246,7 @@ describe('LifecycleUtils::compareTransitions', () => { }); it('should return undefined if no rules given', () => { - const result = lutils.compareTransitions({ }); + const result = lutils.compareTransitions({}); assert.strictEqual(result, undefined); });