From 15a6cd414c02d1204128d57aa8a331314d16277f Mon Sep 17 00:00:00 2001 From: Steve Benedick Date: Tue, 27 Oct 2020 16:43:51 -0600 Subject: [PATCH 1/2] #428 - prettier logs (#429) * #428 - prettier logs * -fix a format issue * -disable indent warning for format in description method of event * -increase timeout for a test and fix typo --- AEPCore/Sources/eventhub/Event.swift | 12 ++++++++- AEPCore/Sources/eventhub/EventHub.swift | 26 +++++++++---------- AEPIdentity/Sources/PushIDManager.swift | 2 +- .../LifecycleIntegrationTests.swift | 14 +++++----- AEPLifecycle/Sources/Lifecycle.swift | 2 +- .../Sources/cache/DiskCacheService.swift | 2 +- 6 files changed, 34 insertions(+), 24 deletions(-) diff --git a/AEPCore/Sources/eventhub/Event.swift b/AEPCore/Sources/eventhub/Event.swift index 5a57636f1..233d8c06b 100644 --- a/AEPCore/Sources/eventhub/Event.swift +++ b/AEPCore/Sources/eventhub/Event.swift @@ -39,7 +39,17 @@ public class Event: NSObject, Codable { /// Event description used for logging @objc override public var description: String { - return "id: \(id.uuidString) name: \(name) type: \(type) source: \(source) data: \(String(describing: data)) timestamp: \(timestamp.description) responseId: \(String(describing: responseID?.uuidString))" + // swiftformat:disable indent + return "\n[\n" + + " id: \(id.uuidString)\n" + + " name: \(name)\n" + + " type: \(type)\n" + + " source: \(source)\n" + + " data: \(data as AnyObject)\n" + + " timestamp: \(timestamp.description)\n" + + " responseId: \(String(describing: responseID?.uuidString))\n" + + "]" + // swiftformat:enable indent } /// Creates a new `Event` with the given parameters diff --git a/AEPCore/Sources/eventhub/EventHub.swift b/AEPCore/Sources/eventhub/EventHub.swift index a0ddcfb36..5e1dfb8d2 100644 --- a/AEPCore/Sources/eventhub/EventHub.swift +++ b/AEPCore/Sources/eventhub/EventHub.swift @@ -74,7 +74,7 @@ final class EventHub { eventHubQueue.async { self.eventQueue.start() self.shareEventHubSharedState() // share state of all registered extensions - Log.debug(label: "\(self.LOG_TAG):\(#function)", "Event Hub successfully started") + Log.debug(label: self.LOG_TAG, "Event Hub successfully started") } } @@ -84,7 +84,7 @@ final class EventHub { // Set an event number for the event eventNumberMap[event.id] = eventNumberCounter.incrementAndGet() eventQueue.add(event) - Log.trace(label: "\(LOG_TAG):\(#function)", "Event #\(String(describing: eventNumberMap[event.id] ?? 0)), \(event) is dispatched.") + Log.trace(label: LOG_TAG, "Dispatching Event #\(String(describing: eventNumberMap[event.id] ?? 0)) - \(event)") } /// Registers a new `Extension` to the `EventHub`. This `Extension` must implement `Extension` @@ -94,7 +94,7 @@ final class EventHub { func registerExtension(_ type: Extension.Type, completion: @escaping (_ error: EventHubError?) -> Void) { eventHubQueue.async { guard !type.typeName.isEmpty else { - Log.warning(label: "\(self.LOG_TAG):\(#function)", "Extension name must not be empty.") + Log.warning(label: self.LOG_TAG, "Extension name must not be empty.") completion(.invalidExtensionName) return } @@ -108,7 +108,7 @@ final class EventHub { let extensionQueue = DispatchQueue(label: "com.adobe.eventhub.extension.\(type.typeName)") let extensionContainer = ExtensionContainer(type, extensionQueue, completion: completion) self.registeredExtensions[type.typeName] = extensionContainer - Log.debug(label: "\(self.LOG_TAG):\(#function)", "\(type.typeName) successfully registered.") + Log.debug(label: self.LOG_TAG, "\(type.typeName) successfully registered.") } } @@ -119,7 +119,7 @@ final class EventHub { func unregisterExtension(_ type: Extension.Type, completion: @escaping (_ error: EventHubError?) -> Void) { eventHubQueue.async { guard self.registeredExtensions[type.typeName] != nil else { - Log.error(label: "\(self.LOG_TAG):\(#function)", "Cannot unregister an extension that is not registered.") + Log.error(label: self.LOG_TAG, "Cannot unregister an extension that is not registered.") completion(.extensionNotRegistered) return } @@ -156,13 +156,13 @@ final class EventHub { /// - event: `Event` for which the `SharedState` should be versioned func createSharedState(extensionName: String, data: [String: Any]?, event: Event?) { guard let (sharedState, version) = versionSharedState(extensionName: extensionName, event: event) else { - Log.warning(label: "\(LOG_TAG):\(#function)", "Error creating shared state for \(extensionName).") + Log.warning(label: LOG_TAG, "Error creating shared state for \(extensionName)") return } sharedState.set(version: version, data: data) dispatch(event: createSharedStateEvent(extensionName: extensionName)) - Log.debug(label: "\(LOG_TAG):\(#function)", "Shared state is created for \(extensionName) with data \(String(describing: data)) and version \(version)") + Log.debug(label: LOG_TAG, "Shared state created for \(extensionName) with version \(version) and data: \n\(data as AnyObject)") } /// Sets the `SharedState` for the extension to pending at `event`'s version and returns a `SharedStateResolver` which is to be invoked with data for the `SharedState` once available. @@ -179,12 +179,12 @@ final class EventHub { if let (sharedState, version) = versionSharedState(extensionName: extensionName, event: event) { pendingVersion = version sharedState.addPending(version: version) - Log.debug(label: "\(LOG_TAG):\(#function)", "Pending shared state is created for \(extensionName) with version \(version)") + Log.debug(label: LOG_TAG, "Pending shared state created for \(extensionName) with version \(version)") } return { [weak self] data in self?.resolvePendingSharedState(extensionName: extensionName, version: pendingVersion, data: data) - Log.debug(label: "\(self?.LOG_TAG ?? "EventHub"):\(#function)", "Pending shared state is resolved for \(extensionName) with data \(String(describing: data)) and version \(String(describing: pendingVersion))") + Log.debug(label: self?.LOG_TAG ?? "EventHub", "Pending shared state resolved for \(extensionName) with version \(String(describing: pendingVersion)) and data: \n\(data as AnyObject)") } } @@ -196,7 +196,7 @@ final class EventHub { /// - Returns: The `SharedState` data and status for the extension with `extensionName` func getSharedState(extensionName: String, event: Event?, barrier: Bool = true) -> SharedStateResult? { guard let container = registeredExtensions.first(where: { $1.sharedStateName == extensionName })?.value, let sharedState = container.sharedState else { - Log.warning(label: "\(LOG_TAG):\(#function)", "Unable to retrieve shared state for \(extensionName). No such extension is registered.") + Log.warning(label: LOG_TAG, "Unable to retrieve shared state for \(extensionName). No such extension is registered.") return nil } @@ -248,14 +248,14 @@ final class EventHub { EventHubConstants.EventDataKeys.EXTENSIONS: extensionsInfo] guard let sharedState = registeredExtensions.first(where: { $1.sharedStateName == EventHubConstants.NAME })?.value.sharedState else { - Log.warning(label: "\(LOG_TAG):\(#function)", "Extension not registered with EventHub") + Log.warning(label: LOG_TAG, "Extension not registered with EventHub") return } let version = sharedState.resolve(version: 0).value == nil ? 0 : eventNumberCounter.incrementAndGet() sharedState.set(version: version, data: data) dispatch(event: createSharedStateEvent(extensionName: EventHubConstants.NAME)) - Log.debug(label: "\(LOG_TAG):\(#function)", "Shared state is created for \(EventHubConstants.NAME) with data \(String(describing: data)) and version \(version)") + Log.debug(label: LOG_TAG, "Shared state created for \(EventHubConstants.NAME) with version \(version) and data: \n\(data as AnyObject)") } // MARK: - Private @@ -268,7 +268,7 @@ final class EventHub { /// - Returns: A `(SharedState, Int)?` containing the state for the provided extension and its version number private func versionSharedState(extensionName: String, event: Event?) -> (SharedState, Int)? { guard let extensionContainer = registeredExtensions.first(where: { $1.sharedStateName == extensionName })?.value else { - Log.error(label: "\(LOG_TAG):\(#function)", "Extension \(extensionName) not registered with EventHub") + Log.error(label: LOG_TAG, "Extension \(extensionName) not registered with EventHub") return nil } diff --git a/AEPIdentity/Sources/PushIDManager.swift b/AEPIdentity/Sources/PushIDManager.swift index d93f432fb..5c6d2cd07 100644 --- a/AEPIdentity/Sources/PushIDManager.swift +++ b/AEPIdentity/Sources/PushIDManager.swift @@ -99,7 +99,7 @@ struct PushIDManager: PushIDManageable { pushEnabled = enabled let pushStatusStr = enabled ? "True" : "False" let contextData = [IdentityConstants.Analytics.EVENT_PUSH_STATUS: pushStatusStr] - let eventData = [IdentityConstants.Analytics.TRACK_ACTION: IdentityConstants.Analytics.PUSH_ID_ENABLED_ACTION_NAME, + let eventData = [IdentityConstants.Analytics.TRACK_ACTION: IdentityConstants.Analytics.PUSH_ID_ENABLED_ACTION_NAME, IdentityConstants.Analytics.CONTEXT_DATA: contextData, IdentityConstants.Analytics.TRACK_INTERNAL: true] as [String: Any] diff --git a/AEPIntegrationTests/LifecycleIntegrationTests.swift b/AEPIntegrationTests/LifecycleIntegrationTests.swift index a8a4ed6e0..f77a3d073 100644 --- a/AEPIntegrationTests/LifecycleIntegrationTests.swift +++ b/AEPIntegrationTests/LifecycleIntegrationTests.swift @@ -18,7 +18,7 @@ import AEPSignal class LifecycleIntegrationTests: XCTestCase { var mockNetworkService = TestableNetworkService() - let defaultSucsessResponse = HTTPURLResponse(url: URL(string: "https://adobe.com")!, statusCode: 200, httpVersion: nil, headerFields: [:]) + let defaultSuccessResponse = HTTPURLResponse(url: URL(string: "https://adobe.com")!, statusCode: 200, httpVersion: nil, headerFields: [:]) override func setUp() { UserDefaults.clear() @@ -71,7 +71,7 @@ class LifecycleIntegrationTests: XCTestCase { if request.url.absoluteString.starts(with: "https://www.lifecycle.com") { lifecycleExpectation.fulfill() XCTAssertTrue(request.url.absoluteString.contains("installevent=InstallEvent")) - return (data: nil, respsonse: self.defaultSucsessResponse, error: nil) + return (data: nil, respsonse: self.defaultSuccessResponse, error: nil) } return nil } @@ -105,7 +105,7 @@ class LifecycleIntegrationTests: XCTestCase { lifecycleExpectation.fulfill() XCTAssertTrue(request.url.absoluteString.contains("installevent=&")) XCTAssertTrue(request.url.absoluteString.contains("launchevent=LaunchEvent")) - return (data: nil, respsonse: self.defaultSucsessResponse, error: nil) + return (data: nil, respsonse: self.defaultSuccessResponse, error: nil) } return nil } @@ -139,13 +139,13 @@ class LifecycleIntegrationTests: XCTestCase { XCTAssertTrue(request.url.absoluteString.contains("installevent=&")) XCTAssertTrue(request.url.absoluteString.contains("launchevent=LaunchEvent")) XCTAssertTrue(request.url.absoluteString.contains("crashevent=CrashEvent")) - return (data: nil, respsonse: self.defaultSucsessResponse, error: nil) + return (data: nil, respsonse: self.defaultSuccessResponse, error: nil) } return nil } MobileCore.lifecycleStart(additionalContextData: nil) - wait(for: [lifecycleExpectation], timeout: 2) + wait(for: [lifecycleExpectation], timeout: 3) } func testAdditionalContextData() { @@ -165,7 +165,7 @@ class LifecycleIntegrationTests: XCTestCase { if request.url.absoluteString.starts(with: "https://www.lifecycle.com") { lifecycleExpectation.fulfill() XCTAssertTrue(request.url.absoluteString.contains("key=value")) - return (data: nil, respsonse: self.defaultSucsessResponse, error: nil) + return (data: nil, respsonse: self.defaultSuccessResponse, error: nil) } return nil } @@ -198,7 +198,7 @@ class LifecycleIntegrationTests: XCTestCase { mockNetworkService.mock { request in if request.url.absoluteString.starts(with: "https://www.lifecycle.com") { lifecycleExpectation.fulfill() - return (data: nil, respsonse: self.defaultSucsessResponse, error: nil) + return (data: nil, respsonse: self.defaultSuccessResponse, error: nil) } return nil } diff --git a/AEPLifecycle/Sources/Lifecycle.swift b/AEPLifecycle/Sources/Lifecycle.swift index 427ffa24e..7ac29e69c 100644 --- a/AEPLifecycle/Sources/Lifecycle.swift +++ b/AEPLifecycle/Sources/Lifecycle.swift @@ -132,7 +132,7 @@ public class Lifecycle: NSObject, Extension { type: EventType.lifecycle, source: EventSource.responseContent, data: eventData) - Log.trace(label: LifecycleConstants.LOG_TAG, "Dispatching lifecycle start event with data: \(eventData)") + Log.trace(label: LifecycleConstants.LOG_TAG, "Dispatching lifecycle start event with data: \n\(eventData as AnyObject)") dispatch(event: startEvent) } diff --git a/AEPServices/Sources/cache/DiskCacheService.swift b/AEPServices/Sources/cache/DiskCacheService.swift index 904919187..af8dc487f 100644 --- a/AEPServices/Sources/cache/DiskCacheService.swift +++ b/AEPServices/Sources/cache/DiskCacheService.swift @@ -26,7 +26,7 @@ class DiskCacheService: Caching { let path = filePath(for: cacheName, with: key) _ = fileManager.createFile(atPath: path, contents: entry.data, attributes: nil) try fileManager.setAttributes([.modificationDate: entry.expiry.date], ofItemAtPath: path) - Log.trace(label: LOG_PREFIX, "Setting key '\(key)' to value '\(String(describing: entry.metadata))' for cache '\(cacheName)'.") + Log.trace(label: LOG_PREFIX, "Updating cache '\(cacheName)' - setting key '\(key)' to value: \n\(entry.metadata as AnyObject)") dataStore.set(key: dataStoreKey(for: cacheName, with: key), value: entry.metadata) } From 2dfd92915a6b981ab05072467fd6b276749e90b5 Mon Sep 17 00:00:00 2001 From: Steve Benedick Date: Mon, 2 Nov 2020 16:13:25 -0700 Subject: [PATCH 2/2] -fix date formatting and add tests (#434) * -fix date formatting and add tests * -fix broken test * -update names of timestamps to be more appropriate * -fix timezone representation for GMT timezone * -cleaner implementation of one of the date formatters * -update test * -try to fix test again * -fix tests for real * -trying to fix tests again * -last fix * -Z * -done --- AEPCore.xcodeproj/project.pbxproj | 6 +- AEPCore/Sources/rules/TokenFinder.swift | 8 +- .../Tests/RulesTests/TokenFinderTests.swift | 17 +- AEPServices/Sources/utility/Date+Format.swift | 14 +- .../Tests/utility/Date+FormatTests.swift | 152 ++++++++++++++++++ 5 files changed, 178 insertions(+), 19 deletions(-) create mode 100644 AEPServices/Tests/utility/Date+FormatTests.swift diff --git a/AEPCore.xcodeproj/project.pbxproj b/AEPCore.xcodeproj/project.pbxproj index 87cb0aceb..53231e0d7 100644 --- a/AEPCore.xcodeproj/project.pbxproj +++ b/AEPCore.xcodeproj/project.pbxproj @@ -60,6 +60,7 @@ 24543A1624E1DC95002D8D9A /* MockUnzipper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2467E43C24CB54B70022F6BE /* MockUnzipper.swift */; }; 2467E43A24CA4DE20022F6BE /* Unzipping.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2467E43924CA4DE20022F6BE /* Unzipping.swift */; }; 247FBD7D24E331A600FA6505 /* Event+SignalTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 247FBD7C24E331A600FA6505 /* Event+SignalTests.swift */; }; + 249498E2254A0C920045E392 /* Date+FormatTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 249498E0254A0C910045E392 /* Date+FormatTests.swift */; }; 24B4935824D4C31100AA38D9 /* AEPSignal.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 24B4934F24D4C31100AA38D9 /* AEPSignal.framework */; }; 24B4935D24D4C31100AA38D9 /* SignalTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 24B4935C24D4C31100AA38D9 /* SignalTests.swift */; }; 24B4935F24D4C31100AA38D9 /* AEPSignal.h in Headers */ = {isa = PBXBuildFile; fileRef = 24B4935124D4C31100AA38D9 /* AEPSignal.h */; settings = {ATTRIBUTES = (Public, ); }; }; @@ -591,6 +592,7 @@ 2467E43B24CB54B70022F6BE /* MockDiskCache.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = MockDiskCache.swift; path = AEPServices/Mocks/MockDiskCache.swift; sourceTree = SOURCE_ROOT; }; 2467E43C24CB54B70022F6BE /* MockUnzipper.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = MockUnzipper.swift; path = AEPServices/Mocks/MockUnzipper.swift; sourceTree = SOURCE_ROOT; }; 247FBD7C24E331A600FA6505 /* Event+SignalTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Event+SignalTests.swift"; sourceTree = ""; }; + 249498E0254A0C910045E392 /* Date+FormatTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Date+FormatTests.swift"; sourceTree = ""; }; 2499461B24E5E67700D3F7B2 /* Package.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Package.swift; sourceTree = ""; }; 24B4934F24D4C31100AA38D9 /* AEPSignal.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = AEPSignal.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 24B4935124D4C31100AA38D9 /* AEPSignal.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = AEPSignal.h; sourceTree = ""; }; @@ -1349,12 +1351,12 @@ 3F0397E724BE60910019F095 /* unzip */, 3F0397F124BE60910019F095 /* AnyCodable.swift */, 3F0397E624BE60910019F095 /* AtomicCounter.swift */, + BB00E26A24D8C9A600C578C1 /* Date+Format.swift */, 3F0397EE24BE60910019F095 /* OperationOrderer.swift */, 2107F02D24C9FF88002935CF /* SHA256.swift */, 3F0397ED24BE60910019F095 /* ThreadSafeArray.swift */, 3F0397EF24BE60910019F095 /* ThreadSafeDictionary.swift */, 3F0397F024BE60910019F095 /* URLEncoder.swift */, - BB00E26A24D8C9A600C578C1 /* Date+Format.swift */, ); path = utility; sourceTree = ""; @@ -1408,6 +1410,7 @@ isa = PBXGroup; children = ( 3F03981124BE61520019F095 /* AnyCodableTests.swift */, + 249498E0254A0C910045E392 /* Date+FormatTests.swift */, 3F03980F24BE61520019F095 /* OperationOrdererTests.swift */, 3F03981024BE61520019F095 /* PersistentHitQueueTests.swift */, 2107F02F24C9FFB2002935CF /* SHA256Tests.swift */, @@ -2585,6 +2588,7 @@ buildActionMask = 2147483647; files = ( 3F03981624BE61520019F095 /* SystemInfoServiceTest.swift in Sources */, + 249498E2254A0C920045E392 /* Date+FormatTests.swift in Sources */, 3F03981E24BE61520019F095 /* DataQueueTests.swift in Sources */, 3F03981524BE61520019F095 /* UnzipperTest.swift in Sources */, 3F03982424BE61520019F095 /* AnyCodableTests.swift in Sources */, diff --git a/AEPCore/Sources/rules/TokenFinder.swift b/AEPCore/Sources/rules/TokenFinder.swift index d5580631d..431b63620 100644 --- a/AEPCore/Sources/rules/TokenFinder.swift +++ b/AEPCore/Sources/rules/TokenFinder.swift @@ -36,8 +36,8 @@ class TokenFinder: Traversable { private let TOKEN_KEY_EVENT_TYPE = "~type" private let TOKEN_KEY_EVENT_SOURCE = "~source" private let TOKEN_KEY_TIMESTAMP_UNIX = "~timestampu" - private let TOKEN_KEY_TIMESTAMP_ISO8601 = "~timestampz" - private let TOKEN_KEY_TIMESTAMP_PLATFORM = "~timestampp" + private let TOKEN_KEY_TIMESTAMP_ISO8601_NO_COLON = "~timestampz" + private let TOKEN_KEY_TIMESTAMP_ISO8601 = "~timestampp" private let TOKEN_KEY_SDK_VERSION = "~sdkver" private let TOKEN_KEY_CACHEBUST = "~cachebust" private let TOKEN_KEY_ALL_URL = "~all_url" @@ -65,9 +65,9 @@ class TokenFinder: Traversable { return event.source case TOKEN_KEY_TIMESTAMP_UNIX: return now.getUnixTimeInSeconds() + case TOKEN_KEY_TIMESTAMP_ISO8601_NO_COLON: + return now.getISO8601DateNoColon() case TOKEN_KEY_TIMESTAMP_ISO8601: - return now.getRFC822Date() - case TOKEN_KEY_TIMESTAMP_PLATFORM: return now.getISO8601Date() case TOKEN_KEY_SDK_VERSION: return MobileCore.extensionVersion diff --git a/AEPCore/Tests/RulesTests/TokenFinderTests.swift b/AEPCore/Tests/RulesTests/TokenFinderTests.swift index 985b3c246..106e780b3 100644 --- a/AEPCore/Tests/RulesTests/TokenFinderTests.swift +++ b/AEPCore/Tests/RulesTests/TokenFinderTests.swift @@ -110,20 +110,21 @@ class TokenFinderTests: XCTestCase { /// Given: initialize `TokenFinder` with mocked extension runtime & dummy event let runtime = TestableExtensionRuntime() let tokenFinder = TokenFinder(event: Event(name: "eventName", type: "eventType", source: "eventSource", data: nil), extensionRuntime: runtime) - let formatter_ISO8601 = DateFormatter() - formatter_ISO8601.locale = Locale(identifier: "en_US_POSIX") - formatter_ISO8601.setLocalizedDateFormatFromTemplate("yyyy-MM-dd'T'HH:mm:ssZZZ") - let formatter_PLATFORM = DateFormatter() - formatter_PLATFORM.locale = Locale(identifier: "en_US_POSIX") - formatter_PLATFORM.setLocalizedDateFormatFromTemplate("yyyy-MM-dd'T'HH:mm:ssXXX") + let formatter_ISO8601 = ISO8601DateFormatter() + formatter_ISO8601.timeZone = TimeZone.current + formatter_ISO8601.formatOptions.insert(.withInternetDateTime) + let formatter_ISO8601NoColon = DateFormatter() + formatter_ISO8601NoColon.locale = Locale(identifier: "en_US_POSIX") + formatter_ISO8601NoColon.dateFormat = "yyyy-MM-dd'T'HH:mm:ssZZZ" + /// When: retrieve token `~timestampz`, `~timestampp` & `~timestampu` - guard let date_ISO8601_string = tokenFinder.get(key: "~timestampz") as? String, let date_ISO8601 = formatter_ISO8601.date(from: date_ISO8601_string), let date_PLATFORM_string = tokenFinder.get(key: "~timestampp") as? String, let date_PLATFORM = formatter_PLATFORM.date(from: date_PLATFORM_string), let date_UNIX_Int64 = tokenFinder.get(key: "~timestampu") as? Int64 else { + guard let date_ISO8601_string = tokenFinder.get(key: "~timestampp") as? String, let date_ISO8601 = formatter_ISO8601.date(from: date_ISO8601_string), let date_ISO8601NoColon_string = tokenFinder.get(key: "~timestampz") as? String, let date_ISO8601NoColon = formatter_ISO8601NoColon.date(from: date_ISO8601NoColon_string), let date_UNIX_Int64 = tokenFinder.get(key: "~timestampu") as? Int64 else { XCTFail("Expected no-nil timestamp") return } let date_UNIX = Date(timeIntervalSince1970: TimeInterval(date_UNIX_Int64)) /// Then: return same timestamp with different format - XCTAssertEqual(date_ISO8601, date_PLATFORM) + XCTAssertEqual(date_ISO8601, date_ISO8601NoColon) XCTAssertEqual(date_ISO8601, date_UNIX) } diff --git a/AEPServices/Sources/utility/Date+Format.swift b/AEPServices/Sources/utility/Date+Format.swift index 287b4b895..82fb53712 100644 --- a/AEPServices/Sources/utility/Date+Format.swift +++ b/AEPServices/Sources/utility/Date+Format.swift @@ -16,17 +16,19 @@ public extension Date { return Int64(timeIntervalSince1970) } - func getRFC822Date() -> String { - let formatter = DateFormatter() - formatter.locale = Locale(identifier: "en_US_POSIX") - formatter.setLocalizedDateFormatFromTemplate("yyyy-MM-dd'T'HH:mm:ssZZZ") + // e.g. - 2020-10-28T15:08:32-06:00 + func getISO8601Date() -> String { + let formatter = ISO8601DateFormatter() + formatter.timeZone = TimeZone.current + formatter.formatOptions.insert(.withInternetDateTime) return formatter.string(from: self) } - func getISO8601Date() -> String { + // e.g. - 2020-10-28T15:08:32-0600 + func getISO8601DateNoColon() -> String { let formatter = DateFormatter() formatter.locale = Locale(identifier: "en_US_POSIX") - formatter.setLocalizedDateFormatFromTemplate("yyyy-MM-dd'T'HH:mm:ssXXX") + formatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ssXX" return formatter.string(from: self) } } diff --git a/AEPServices/Tests/utility/Date+FormatTests.swift b/AEPServices/Tests/utility/Date+FormatTests.swift new file mode 100644 index 000000000..2d90e6408 --- /dev/null +++ b/AEPServices/Tests/utility/Date+FormatTests.swift @@ -0,0 +1,152 @@ +/* + Copyright 2020 Adobe. All rights reserved. + This file is licensed to you under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. You may obtain a copy + of the License at http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software distributed under + the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS + OF ANY KIND, either express or implied. See the License for the specific language + governing permissions and limitations under the License. + */ + +import XCTest + +@testable import AEPServices + +class DateFormatTests: XCTestCase { + + func testGetUnixTimeInSeconds() { + // setup + let victory: Int64 = 1391373045000 // Feb 2, 2014 8:30:45 pm MST + let date = Date(milliseconds: victory) + + // test + let result = date.getUnixTimeInSeconds() + + // verify + XCTAssertEqual(victory, result * 1000) + } + + func testGetISO8601Date() { + // setup + let tzOffset = TimeZone.current.secondsFromGMT() + let victory: Int64 = Int64(tzOffset * 1000) + 1391398245000 // Feb 2, 2014 8:30:45 pm MST + let date = Date(milliseconds: victory) + let expectedDateString = getLocalExpectedDateStringFrom(date) + timezoneStringWithColon + + // test + let result = date.getISO8601Date() + + // verify + XCTAssertEqual(expectedDateString, result) + } + + func testGetISO8601DateNoColon() { + // setup + let tzOffset = TimeZone.current.secondsFromGMT() + let victory: Int64 = Int64(tzOffset * 1000) + 1391398245000 // Feb 2, 2014 8:30:45 pm MST + let date = Date(milliseconds: victory) + let expectedDateString = getLocalExpectedDateStringFrom(date) + timezoneString + + // test + let result = date.getISO8601DateNoColon() + + // verify + XCTAssertEqual(expectedDateString, result) + } + + // MARK: - Helpers + + /// [milliseconds offset from GMT : hours offset from GMT] + let timezoneMapper: [Int:String] = [ + 43200: "+1200", + 39600: "+1100", + 36000: "+1000", + 32400: "+0900", + 28800: "+0800", + 25200: "+0700", + 21600: "+0600", + 18000: "+0500", + 14400: "+0400", + 10800: "+0300", + 7200: "+0200", + 3600: "+0100", + 0: "Z", + -3600: "-0100", + -7200: "-0200", + -10800: "-0300", + -14400: "-0400", + -18000: "-0500", + -21600: "-0600", // US Mountain Standard + -25200: "-0700", // US Mountain Daylight, US Pacific Standard + -28800: "-0800", // US Pacific Daylight + -32400: "-0900", + -36000: "-1000", + -39600: "-1100", + -43200: "-1200", + ] + + let timezoneMapperWithColon: [Int:String] = [ + 43200: "+12:00", + 39600: "+11:00", + 36000: "+10:00", + 32400: "+09:00", + 28800: "+08:00", + 25200: "+07:00", + 21600: "+06:00", + 18000: "+05:00", + 14400: "+04:00", + 10800: "+03:00", + 7200: "+02:00", + 3600: "+01:00", + 0: "Z", + -3600: "-01:00", + -7200: "-02:00", + -10800: "-03:00", + -14400: "-04:00", + -18000: "-05:00", + -21600: "-06:00", // US Mountain Standard + -25200: "-07:00", // US Mountain Daylight, US Pacific Standard + -28800: "-08:00", // US Pacific Daylight + -32400: "-09:00", + -36000: "-10:00", + -39600: "-11:00", + -43200: "-12:00", + ] + + var timezoneString: String { + if TimeZone.current.isDaylightSavingTime() { + return timezoneMapper[TimeZone.current.secondsFromGMT() - 3600] ?? "" + } else { + return timezoneMapper[TimeZone.current.secondsFromGMT()] ?? "" + } + } + + var timezoneStringWithColon: String { + if TimeZone.current.isDaylightSavingTime() { + return timezoneMapperWithColon[TimeZone.current.secondsFromGMT() - 3600] ?? "" + } else { + return timezoneMapperWithColon[TimeZone.current.secondsFromGMT()] ?? "" + } + } + + func getLocalExpectedDateStringFrom(_ date: Date) -> String { + let formatter = DateFormatter() + formatter.timeZone = TimeZone.current + formatter.dateFormat = "YYYY" + let year = formatter.string(from: date) + formatter.dateFormat = "MM" + let month = formatter.string(from: date) + formatter.dateFormat = "dd" + let day = formatter.string(from: date) + formatter.dateFormat = "HH" + let hours = formatter.string(from: date) + formatter.dateFormat = "mm" + let minutes = formatter.string(from: date) + formatter.dateFormat = "ss" + let seconds = formatter.string(from: date) + + return "\(year)-\(month)-\(day)T\(hours):\(minutes):\(seconds)" + } +}