diff --git a/UnitTests/ObjCTests/MPPersistenceControllerTests.m b/UnitTests/ObjCTests/MPPersistenceControllerTests.m index 2cdb0687b..21709dea0 100644 --- a/UnitTests/ObjCTests/MPPersistenceControllerTests.m +++ b/UnitTests/ObjCTests/MPPersistenceControllerTests.m @@ -17,6 +17,8 @@ #import "MPBaseTestCase.h" #import "MPStateMachine.h" #import "MPKitFilter.h" +#import "UploadSettingsUtils.h" +#import "MPUploadSettings.h" @interface MParticle () @@ -1458,4 +1460,86 @@ - (void)testMaxBytesPerBatch { XCTAssertGreaterThan(maxBytesCrash, maxBytesNormal, @"Crash batch should allow more bytes than normal batch"); } +// Simulates a row written by the old Swift module (mParticle_Apple_SDK_NoLocation) +// by manually encoding MPUploadSettings under the legacy class name and verifying +// fetchUploads can still decode and return it after the ObjC migration. +- (void)testFetchUploads_decodesLegacySwiftModuleClassName { + MPSession *session = [[MPSession alloc] initWithStartTime:[[NSDate date] timeIntervalSince1970] + userId:[MPPersistenceController_PRIVATE mpId]]; + NSDictionary *uploadDictionary = @{kMPOptOutKey: @NO, + kMPSessionTimeoutKey: @120, + kMPUploadIntervalKey: @10, + kMPLifeTimeValueKey: @0, + kMPMessagesKey: @[], + kMPMessageIdKey: [[NSUUID UUID] UUIDString]}; + + MPUploadSettings *settings = [MPUploadSettings currentUploadSettingsWithStateMachine:[MParticle sharedInstance].stateMachine + networkOptions:[MParticle sharedInstance].networkOptions]; + + // Encode using the legacy Swift module class name to simulate pre-migration persisted data + [NSKeyedArchiver setClassName:@"mParticle_Apple_SDK_NoLocation.MPUploadSettings" + forClass:[MPUploadSettings class]]; + NSData *legacyBlob = [NSKeyedArchiver archivedDataWithRootObject:settings + requiringSecureCoding:YES + error:nil]; + // Reset so other tests are not affected + [NSKeyedArchiver setClassName:nil forClass:[MPUploadSettings class]]; + + MPUpload *upload = [[MPUpload alloc] initWithSessionId:@(session.sessionId) + uploadDictionary:uploadDictionary + dataPlanId:nil + dataPlanVersion:nil + uploadSettings:settings]; + + MPPersistenceController_PRIVATE *persistence = [MParticle sharedInstance].persistenceController; + [persistence saveUpload:upload]; + + // Patch the saved row's upload_settings blob with the legacy-encoded version + // to simulate the state of a DB row written before the Swift→ObjC migration + NSString *dbPath = [persistence valueForKey:@"databasePath"]; + sqlite3 *db; + sqlite3_open([dbPath UTF8String], &db); + sqlite3_stmt *stmt; + sqlite3_prepare_v2(db, "UPDATE uploads SET upload_settings = ? WHERE _id = ?", -1, &stmt, NULL); + sqlite3_bind_blob(stmt, 1, legacyBlob.bytes, (int)legacyBlob.length, SQLITE_TRANSIENT); + sqlite3_bind_int64(stmt, 2, upload.uploadId); + sqlite3_step(stmt); + sqlite3_finalize(stmt); + sqlite3_close(db); + + NSArray *fetched = [persistence fetchUploads]; + XCTAssertEqual(fetched.count, 1, @"Upload with legacy class name should be decoded and returned by fetchUploads"); + XCTAssertEqualObjects(fetched.firstObject.uploadSettings.apiKey, settings.apiKey, + @"Decoded upload settings apiKey should match original"); +} + +// Verifies registerUploadSettingsClassMappings enables decoding blobs encoded +// under either the old Swift module name or the current ObjC module name. +- (void)testRegisterUploadSettingsClassMappings_decodesAllModuleNames { + MPUploadSettings *original = [MPUploadSettings currentUploadSettingsWithStateMachine:[MParticle sharedInstance].stateMachine + networkOptions:[MParticle sharedInstance].networkOptions]; + + NSArray *legacyNames = @[ + @"mParticle_Apple_SDK_NoLocation.MPUploadSettings", + @"mParticle_Apple_SDK.MPUploadSettings" + ]; + + for (NSString *legacyName in legacyNames) { + [NSKeyedArchiver setClassName:legacyName forClass:[MPUploadSettings class]]; + NSData *blob = [NSKeyedArchiver archivedDataWithRootObject:original + requiringSecureCoding:YES + error:nil]; + [NSKeyedArchiver setClassName:nil forClass:[MPUploadSettings class]]; + + NSError *error = nil; + MPUploadSettings *decoded = [NSKeyedUnarchiver unarchivedObjectOfClass:[MPUploadSettings class] + fromData:blob + error:&error]; + XCTAssertNil(error, @"Should decode without error for class name: %@", legacyName); + XCTAssertNotNil(decoded, @"Decoded settings should not be nil for class name: %@", legacyName); + XCTAssertEqualObjects(decoded.apiKey, original.apiKey, + @"apiKey should survive round-trip for class name: %@", legacyName); + } +} + @end diff --git a/mParticle-Apple-SDK/Utils/MPUploadSettings.m b/mParticle-Apple-SDK/Utils/MPUploadSettings.m index bd1188df6..50abc5374 100644 --- a/mParticle-Apple-SDK/Utils/MPUploadSettings.m +++ b/mParticle-Apple-SDK/Utils/MPUploadSettings.m @@ -14,6 +14,15 @@ @implementation MPUploadSettings ++ (void)initialize { + if (self == [MPUploadSettings class]) { + [NSKeyedUnarchiver setClass:[MPUploadSettings class] + forClassName:@"mParticle_Apple_SDK.MPUploadSettings"]; + [NSKeyedUnarchiver setClass:[MPUploadSettings class] + forClassName:@"mParticle_Apple_SDK_NoLocation.MPUploadSettings"]; + } +} + + (BOOL)supportsSecureCoding { return YES; } diff --git a/mParticle-Apple-SDK/Utils/UploadSettingsUtils.m b/mParticle-Apple-SDK/Utils/UploadSettingsUtils.m index 166636ffc..422e6ea80 100644 --- a/mParticle-Apple-SDK/Utils/UploadSettingsUtils.m +++ b/mParticle-Apple-SDK/Utils/UploadSettingsUtils.m @@ -3,7 +3,6 @@ @implementation UploadSettingsUtils - + (void)setLastUploadSettings:(nullable MPUploadSettings *)lastUploadSettings userDefaults:(MPUserDefaults*)userDefaults { if (lastUploadSettings) { NSError *error = nil; @@ -31,8 +30,6 @@ + (nullable MPUploadSettings*)lastUploadSettingsWithUserDefaults:(MPUserDefaults } NSError *error = nil; - [NSKeyedArchiver setClassName:@"mParticle_Apple_SDK.MPUploadSettings" forClass:MPUploadSettings.self]; - [NSKeyedArchiver setClassName:@"mParticle_Apple_SDK_NoLocation.MPUploadSettings" forClass:MPUploadSettings.self]; MPUploadSettings *settings = [NSKeyedUnarchiver unarchivedObjectOfClass:[MPUploadSettings class] fromData:(NSData *)obj