From 78971fd2b38fe479a13627b38c46187ea06f9ca9 Mon Sep 17 00:00:00 2001 From: Bruno Kambere Date: Sun, 29 Mar 2026 19:32:02 +0300 Subject: [PATCH 1/3] Fix SOAP stdClass conversion for array|string union types --- src/API/TypeConverter.php | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/src/API/TypeConverter.php b/src/API/TypeConverter.php index 91e770ff..3b044f0d 100644 --- a/src/API/TypeConverter.php +++ b/src/API/TypeConverter.php @@ -35,6 +35,19 @@ public static function convertValueToExpectedType($object, $value, string $prope */ public static function convertToType($value, string $type) { + // Handle special case for 'array' type + if ($type === 'array') { + if (is_array($value)) { + return $value; + } + if ($value instanceof \stdClass) { + $props = get_object_vars($value); + $arr = count($props) === 1 ? current($props) : (array)$value; + return is_array($arr) ? $arr : [$arr]; + } + return is_array($value) ? $value : [$value]; + } + if (!class_exists($type)) { return $value; } @@ -84,6 +97,10 @@ private static function getSetterType($object, string $property): ?string if (!$t->isBuiltin() && $t->getName() !== 'array') { return $t->getName(); } + // Return 'array' as the conversion target type for union types that include it + if ($t->getName() === 'array') { + return 'array'; + } } } From 7794e3e32782d3260cdccb49b41d0de5bf930b7d Mon Sep 17 00:00:00 2001 From: Bruno Kambere Date: Sun, 29 Mar 2026 20:39:25 +0300 Subject: [PATCH 2/3] Add test for stdClass conversion with array|string union types --- tests/src/API/TypeConverterTest.php | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/tests/src/API/TypeConverterTest.php b/tests/src/API/TypeConverterTest.php index 35bfbc92..38cf8258 100644 --- a/tests/src/API/TypeConverterTest.php +++ b/tests/src/API/TypeConverterTest.php @@ -14,6 +14,7 @@ use garethp\ews\API\Type\FolderType; use garethp\ews\API\Type\PermissionSetType; use garethp\ews\API\Type\PermissionType; +use garethp\ews\API\Type\ContactType; use PHPUnit\Framework\TestCase; class TypeConverterTest extends TestCase @@ -169,4 +170,26 @@ public function testHandlesNonStdClassData() $this->assertCount(1, $headers, 'Expected 1 header to be present'); $this->assertSame($properHeader, $headers[0], 'Expected header to match the original InternetHeaderType instance'); } + + public function testConvertStdClassToArrayForUnionType() + { + $contact = new ContactType(); + + // Simulate SOAP response with EmailAddresses as stdClass + $soapData = (object)[ + 'Name' => 'test name', + 'Email' => 'test@example.com' + ]; + + // This should convert stdClass to array for array|string type + $contact->emailAddresses = $soapData; + + $emails = $contact->getEmailAddresses(); + + $this->assertIsArray($emails, 'Expected emailAddresses to be converted to array'); + $this->assertArrayHasKey('Name', $emails, 'Expected Name property in converted array'); + $this->assertArrayHasKey('Email', $emails, 'Expected Email property in converted array'); + $this->assertEquals('test name', $emails['Name'], 'Expected Name value to match'); + $this->assertEquals('test@example.com', $emails['Email'], 'Expected Email value to match'); + } } From 6c56def8f974c17bcadc2b92718abc06c29f994c Mon Sep 17 00:00:00 2001 From: Bruno Kambere Date: Thu, 9 Apr 2026 23:27:45 +0300 Subject: [PATCH 3/3] Add integration test for handling Categories field with array|string union type in CalendarItem --- ...est.testGetCalendarItemWithCategories.json | 1 + tests/src/API/TypeConverterTest.php | 23 ----------- tests/src/Calendar/CalendarTest.php | 38 +++++++++++++++++++ 3 files changed, 39 insertions(+), 23 deletions(-) create mode 100644 Resources/recordings/garethp/ews/Test/Calendar/CalendarTest.testGetCalendarItemWithCategories.json diff --git a/Resources/recordings/garethp/ews/Test/Calendar/CalendarTest.testGetCalendarItemWithCategories.json b/Resources/recordings/garethp/ews/Test/Calendar/CalendarTest.testGetCalendarItemWithCategories.json new file mode 100644 index 00000000..c40c9025 --- /dev/null +++ b/Resources/recordings/garethp/ews/Test/Calendar/CalendarTest.testGetCalendarItemWithCategories.json @@ -0,0 +1 @@ +[{"error":false,"statusCode":200,"headers":{"Cache-Control":["private"],"Transfer-Encoding":["chunked"],"Content-Type":["text\/xml; charset=utf-8"],"Server":[""],"request-id":["b387eaab-7b8e-4c33-b93f-d147494a78d5"],"X-CalculatedBETarget":["dag2ex2.indiv.local"],"Strict-Transport-Security":["max-age=31536000; includeSubDomains","max-age=31536000; includeSubDomains"],"X-DiagInfo":["DAG2EX2"],"X-BEServer":["DAG2EX2"],"X-AspNet-Version":["4.0.30319"],"Set-Cookie":["exchangecookie=0a92ad20f8ff46279e8ca85f5fbcd37b; expires=Fri, 09-Apr-2027 20:15:42 GMT; path=\/; HttpOnly","X-BackEndCookie=S-1-5-21-2339374403-141678755-699136657-27032=u56Lnp2ejJqBz5zMmcqZzsjSyMiemtLLz5nO0p3OzMbSxp7KmZydyMzNnJuegYHNz83J0s\/K0s\/Gq83Pxc7KxcvNgZaRm5aJ0ZOQnJ6Tgc8=; expires=Sat, 09-May-2026 20:15:42 GMT; path=\/EWS; secure; HttpOnly"],"Persistent-Auth":["true"],"X-Powered-By":["ASP.NET"],"X-FEServer":["DAG2EX2"],"Date":["Thu, 09 Apr 2026 20:15:42 GMT"]},"body":"<\/s:Header>NoError<\/m:ResponseCode>IPF.Appointment.Birthday<\/t:FolderClass>Birthdays<\/t:DisplayName>0<\/t:TotalCount>0<\/t:ChildFolderCount>true<\/t:CreateAssociated>true<\/t:CreateContents>true<\/t:CreateHierarchy>true<\/t:Delete>true<\/t:Modify>true<\/t:Read><\/t:EffectiveRights><\/t:CalendarFolder>IPF.Appointment<\/t:FolderClass>Test<\/t:DisplayName>3<\/t:TotalCount>0<\/t:ChildFolderCount>true<\/t:CreateAssociated>true<\/t:CreateContents>true<\/t:CreateHierarchy>true<\/t:Delete>true<\/t:Modify>true<\/t:Read><\/t:EffectiveRights><\/t:CalendarFolder><\/t:Folders><\/m:RootFolder><\/m:FindFolderResponseMessage><\/m:ResponseMessages><\/m:FindFolderResponse><\/s:Body><\/s:Envelope>"},{"error":false,"statusCode":200,"headers":{"Cache-Control":["private"],"Transfer-Encoding":["chunked"],"Content-Type":["text\/xml; charset=utf-8"],"Server":[""],"request-id":["3047f266-8600-403c-8e82-a9fc3ee4ce48"],"X-CalculatedBETarget":["dag2ex2.indiv.local"],"Strict-Transport-Security":["max-age=31536000; includeSubDomains","max-age=31536000; includeSubDomains"],"X-DiagInfo":["DAG2EX2"],"X-BEServer":["DAG2EX2"],"X-AspNet-Version":["4.0.30319"],"Set-Cookie":["exchangecookie=c179518671c84baaae89ed183b5a7782; expires=Fri, 09-Apr-2027 20:15:42 GMT; path=\/; HttpOnly","X-BackEndCookie=S-1-5-21-2339374403-141678755-699136657-27032=u56Lnp2ejJqBz5zMmcqZzsjSyMiemtLLz5nO0p3OzMbSxp7KmZydyMzNnJuegYHNz83J0s\/K0s\/Gq83Pxc7KxcvNgZaRm5aJ0ZOQnJ6Tgc8=; expires=Sat, 09-May-2026 20:15:42 GMT; path=\/EWS; secure; HttpOnly"],"X-Powered-By":["ASP.NET"],"X-FEServer":["DAG2EX2"],"Date":["Thu, 09 Apr 2026 20:15:42 GMT"]},"body":"<\/s:Header>NoError<\/m:ResponseCode><\/m:RootFolder><\/m:FindItemResponseMessage><\/m:ResponseMessages><\/m:FindItemResponse><\/s:Body><\/s:Envelope>"},{"error":false,"statusCode":200,"headers":{"Cache-Control":["private"],"Transfer-Encoding":["chunked"],"Content-Type":["text\/xml; charset=utf-8"],"Server":[""],"request-id":["d3abecbd-b390-4e67-b275-1e77d8dfd160"],"X-CalculatedBETarget":["dag2ex2.indiv.local"],"Strict-Transport-Security":["max-age=31536000; includeSubDomains","max-age=31536000; includeSubDomains"],"X-DiagInfo":["DAG2EX2"],"X-BEServer":["DAG2EX2"],"X-AspNet-Version":["4.0.30319"],"Set-Cookie":["exchangecookie=a0e271f7742d463799c87f8e268eb935; expires=Fri, 09-Apr-2027 20:15:43 GMT; path=\/; HttpOnly","X-BackEndCookie=S-1-5-21-2339374403-141678755-699136657-27032=u56Lnp2ejJqBz5zMmcqZzsjSyMiemtLLz5nO0p3OzMbSxp7KmZydyMzNnJuegYHNz83J0s\/K0s\/Gq83Pxc7KxcvMgZaRm5aJ0ZOQnJ6Tgc8=; expires=Sat, 09-May-2026 20:15:43 GMT; path=\/EWS; secure; HttpOnly"],"X-Powered-By":["ASP.NET"],"X-FEServer":["DAG2EX2"],"Date":["Thu, 09 Apr 2026 20:15:42 GMT"]},"body":"<\/s:Header>NoError<\/m:ResponseCode>IPF.Appointment.Birthday<\/t:FolderClass>Birthdays<\/t:DisplayName>0<\/t:TotalCount>0<\/t:ChildFolderCount>true<\/t:CreateAssociated>true<\/t:CreateContents>true<\/t:CreateHierarchy>true<\/t:Delete>true<\/t:Modify>true<\/t:Read><\/t:EffectiveRights><\/t:CalendarFolder>IPF.Appointment<\/t:FolderClass>Test<\/t:DisplayName>3<\/t:TotalCount>0<\/t:ChildFolderCount>true<\/t:CreateAssociated>true<\/t:CreateContents>true<\/t:CreateHierarchy>true<\/t:Delete>true<\/t:Modify>true<\/t:Read><\/t:EffectiveRights><\/t:CalendarFolder><\/t:Folders><\/m:RootFolder><\/m:FindFolderResponseMessage><\/m:ResponseMessages><\/m:FindFolderResponse><\/s:Body><\/s:Envelope>"},{"error":false,"statusCode":200,"headers":{"Cache-Control":["private"],"Transfer-Encoding":["chunked"],"Content-Type":["text\/xml; charset=utf-8"],"Server":[""],"request-id":["49970a07-c93f-4875-9f57-f655d8b37fc5"],"X-CalculatedBETarget":["dag2ex2.indiv.local"],"Strict-Transport-Security":["max-age=31536000; includeSubDomains","max-age=31536000; includeSubDomains"],"X-DiagInfo":["DAG2EX2"],"X-BEServer":["DAG2EX2"],"X-AspNet-Version":["4.0.30319"],"Set-Cookie":["exchangecookie=c95d2ce61a294457ba06de276414566c; expires=Fri, 09-Apr-2027 20:15:43 GMT; path=\/; HttpOnly","X-BackEndCookie=S-1-5-21-2339374403-141678755-699136657-27032=u56Lnp2ejJqBz5zMmcqZzsjSyMiemtLLz5nO0p3OzMbSxp7KmZydyMzNnJuegYHNz83J0s\/K0s\/Gq83Pxc7KxcvMgZaRm5aJ0ZOQnJ6Tgc8=; expires=Sat, 09-May-2026 20:15:43 GMT; path=\/EWS; secure; HttpOnly"],"X-Powered-By":["ASP.NET"],"X-FEServer":["DAG2EX2"],"Date":["Thu, 09 Apr 2026 20:15:43 GMT"]},"body":"<\/s:Header>NoError<\/m:ResponseCode><\/t:CalendarItem><\/m:Items><\/m:CreateItemResponseMessage><\/m:ResponseMessages><\/m:CreateItemResponse><\/s:Body><\/s:Envelope>"},{"error":false,"statusCode":200,"headers":{"Cache-Control":["private"],"Transfer-Encoding":["chunked"],"Content-Type":["text\/xml; charset=utf-8"],"Server":[""],"request-id":["dbb5d2c5-4c77-4aff-9cd2-beaecec57639"],"X-CalculatedBETarget":["dag2ex2.indiv.local"],"Strict-Transport-Security":["max-age=31536000; includeSubDomains","max-age=31536000; includeSubDomains"],"X-DiagInfo":["DAG2EX2"],"X-BEServer":["DAG2EX2"],"X-AspNet-Version":["4.0.30319"],"Set-Cookie":["exchangecookie=316edc18d63d4dec9d5176f9027305b0; expires=Fri, 09-Apr-2027 20:15:43 GMT; path=\/; HttpOnly","X-BackEndCookie=S-1-5-21-2339374403-141678755-699136657-27032=u56Lnp2ejJqBz5zMmcqZzsjSyMiemtLLz5nO0p3OzMbSxp7KmZydyMzNnJuegYHNz83J0s\/K0s\/Gq83Pxc7KxcvMgZaRm5aJ0ZOQnJ6Tgc8=; expires=Sat, 09-May-2026 20:15:43 GMT; path=\/EWS; secure; HttpOnly"],"X-Powered-By":["ASP.NET"],"X-FEServer":["DAG2EX2"],"Date":["Thu, 09 Apr 2026 20:15:43 GMT"]},"body":"<\/s:Header>NoError<\/m:ResponseCode>IPM.Appointment<\/t:ItemClass>Union Type Test Event<\/t:Subject>Normal<\/t:Sensitivity>2026-04-09T20:15:43Z<\/t:DateTimeReceived>4452<\/t:Size>TestCategory<\/t:String>UnionTypeTest<\/t:String><\/t:Categories>Normal<\/t:Importance>false<\/t:IsSubmitted>false<\/t:IsDraft>false<\/t:IsFromMe>false<\/t:IsResend>false<\/t:IsUnmodified>2026-04-09T20:15:43Z<\/t:DateTimeSent>2026-04-09T20:15:43Z<\/t:DateTimeCreated><\/t:ResponseObjects>2026-04-09T10:00:00Z<\/t:ReminderDueBy>true<\/t:ReminderIsSet>15<\/t:ReminderMinutesBeforeStart>false<\/t:HasAttachments>en-US<\/t:Culture>false<\/t:CreateAssociated>false<\/t:CreateContents>false<\/t:CreateHierarchy>true<\/t:Delete>true<\/t:Modify>true<\/t:Read><\/t:EffectiveRights>outlook.example<\/t:LastModifiedName>2026-04-09T20:15:43Z<\/t:LastModifiedTime>false<\/t:IsAssociated>https:\/\/ex.mail.ovh.ca\/owa?itemid=AAMkADk5Njk2ZjM0LTZmMWItNGIwZS05YTBjLTUzZWE4ZTExN2I5OQBGAAAAAADoTzuWq76VRIeNlYRsLpeUBwDwSoBO6tE6QZYs8Ixm76GUAAAzNmMjAADwSoBO6tE6QZYs8Ixm76GUAADnOi1QAAA%3D&exvsurl=1&path=\/calendar\/item<\/t:WebClientReadFormQueryString>040000008200E00074C5B7101A82E00800000000C7D7C1A45DC8DC0100000000000000001000000083549F6AF04C3A45AF8659A9389674F4<\/t:UID>2026-04-09T20:15:43Z<\/t:DateTimeStamp>2026-04-09T10:00:00Z<\/t:Start>2026-04-09T11:00:00Z<\/t:End>false<\/t:IsAllDayEvent>Busy<\/t:LegacyFreeBusyStatus>false<\/t:IsMeeting>false<\/t:IsCancelled>false<\/t:IsRecurring>false<\/t:MeetingRequestWasSent>true<\/t:IsResponseRequested>Single<\/t:CalendarItemType>Organizer<\/t:MyResponseType>outlook.example<\/t:Name>example@outlook.example.com<\/t:EmailAddress>SMTP<\/t:RoutingType>Mailbox<\/t:MailboxType><\/t:Mailbox><\/t:Organizer>PT1H<\/t:Duration>UTC<\/t:TimeZone>0<\/t:AppointmentSequenceNumber>0<\/t:AppointmentState>false<\/t:IsOnlineMeeting><\/t:CalendarItem><\/m:Items><\/m:GetItemResponseMessage><\/m:ResponseMessages><\/m:GetItemResponse><\/s:Body><\/s:Envelope>"},{"error":false,"statusCode":200,"headers":{"Cache-Control":["private"],"Transfer-Encoding":["chunked"],"Content-Type":["text\/xml; charset=utf-8"],"Server":[""],"request-id":["aed2422e-3c65-4386-89e0-241aee48049a"],"X-CalculatedBETarget":["dag2ex2.indiv.local"],"Strict-Transport-Security":["max-age=31536000; includeSubDomains","max-age=31536000; includeSubDomains"],"X-DiagInfo":["DAG2EX2"],"X-BEServer":["DAG2EX2"],"X-AspNet-Version":["4.0.30319"],"Set-Cookie":["exchangecookie=ee3ca6bb59c74eefb5e18b06114c67be; expires=Fri, 09-Apr-2027 20:15:44 GMT; path=\/; HttpOnly","X-BackEndCookie=S-1-5-21-2339374403-141678755-699136657-27032=u56Lnp2ejJqBz5zMmcqZzsjSyMiemtLLz5nO0p3OzMbSxp7KmZydyMzNnJuegYHNz83J0s\/K0s\/Gq83Pxc7KxcvLgZaRm5aJ0ZOQnJ6Tgc8=; expires=Sat, 09-May-2026 20:15:44 GMT; path=\/EWS; secure; HttpOnly"],"X-Powered-By":["ASP.NET"],"X-FEServer":["DAG2EX2"],"Date":["Thu, 09 Apr 2026 20:15:43 GMT"]},"body":"<\/s:Header>NoError<\/m:ResponseCode><\/m:DeleteItemResponseMessage><\/m:ResponseMessages><\/m:DeleteItemResponse><\/s:Body><\/s:Envelope>"},{"error":false,"statusCode":200,"headers":{"Cache-Control":["private"],"Transfer-Encoding":["chunked"],"Content-Type":["text\/xml; charset=utf-8"],"Server":[""],"request-id":["8243a45d-fff5-4ba7-812d-b9bb94b256eb"],"X-CalculatedBETarget":["dag2ex2.indiv.local"],"Strict-Transport-Security":["max-age=31536000; includeSubDomains","max-age=31536000; includeSubDomains"],"X-DiagInfo":["DAG2EX2"],"X-BEServer":["DAG2EX2"],"X-AspNet-Version":["4.0.30319"],"Set-Cookie":["exchangecookie=a09794994d0c4c479f7bebc7565b941d; expires=Fri, 09-Apr-2027 20:15:44 GMT; path=\/; HttpOnly","X-BackEndCookie=S-1-5-21-2339374403-141678755-699136657-27032=u56Lnp2ejJqBz5zMmcqZzsjSyMiemtLLz5nO0p3OzMbSxp7KmZydyMzNnJuegYHNz83J0s\/K0s\/Gq83Pxc7KxcvLgZaRm5aJ0ZOQnJ6Tgc8=; expires=Sat, 09-May-2026 20:15:44 GMT; path=\/EWS; secure; HttpOnly"],"X-Powered-By":["ASP.NET"],"X-FEServer":["DAG2EX2"],"Date":["Thu, 09 Apr 2026 20:15:44 GMT"]},"body":"<\/s:Header>NoError<\/m:ResponseCode>IPF.Appointment.Birthday<\/t:FolderClass>Birthdays<\/t:DisplayName>0<\/t:TotalCount>0<\/t:ChildFolderCount>true<\/t:CreateAssociated>true<\/t:CreateContents>true<\/t:CreateHierarchy>true<\/t:Delete>true<\/t:Modify>true<\/t:Read><\/t:EffectiveRights><\/t:CalendarFolder>IPF.Appointment<\/t:FolderClass>Test<\/t:DisplayName>3<\/t:TotalCount>0<\/t:ChildFolderCount>true<\/t:CreateAssociated>true<\/t:CreateContents>true<\/t:CreateHierarchy>true<\/t:Delete>true<\/t:Modify>true<\/t:Read><\/t:EffectiveRights><\/t:CalendarFolder><\/t:Folders><\/m:RootFolder><\/m:FindFolderResponseMessage><\/m:ResponseMessages><\/m:FindFolderResponse><\/s:Body><\/s:Envelope>"},{"error":false,"statusCode":200,"headers":{"Cache-Control":["private"],"Transfer-Encoding":["chunked"],"Content-Type":["text\/xml; charset=utf-8"],"Server":[""],"request-id":["f2f4d441-8469-4153-919b-5e920d97a934"],"X-CalculatedBETarget":["dag2ex2.indiv.local"],"Strict-Transport-Security":["max-age=31536000; includeSubDomains","max-age=31536000; includeSubDomains"],"X-DiagInfo":["DAG2EX2"],"X-BEServer":["DAG2EX2"],"X-AspNet-Version":["4.0.30319"],"Set-Cookie":["exchangecookie=40731d7a157b45d18d89b6581b935bff; expires=Fri, 09-Apr-2027 20:15:44 GMT; path=\/; HttpOnly","X-BackEndCookie=S-1-5-21-2339374403-141678755-699136657-27032=u56Lnp2ejJqBz5zMmcqZzsjSyMiemtLLz5nO0p3OzMbSxp7KmZydyMzNnJuegYHNz83J0s\/K0s\/Gq83Pxc7KxcvLgZaRm5aJ0ZOQnJ6Tgc8=; expires=Sat, 09-May-2026 20:15:44 GMT; path=\/EWS; secure; HttpOnly"],"X-Powered-By":["ASP.NET"],"X-FEServer":["DAG2EX2"],"Date":["Thu, 09 Apr 2026 20:15:44 GMT"]},"body":"<\/s:Header>NoError<\/m:ResponseCode><\/m:RootFolder><\/m:FindItemResponseMessage><\/m:ResponseMessages><\/m:FindItemResponse><\/s:Body><\/s:Envelope>"}] \ No newline at end of file diff --git a/tests/src/API/TypeConverterTest.php b/tests/src/API/TypeConverterTest.php index 38cf8258..35bfbc92 100644 --- a/tests/src/API/TypeConverterTest.php +++ b/tests/src/API/TypeConverterTest.php @@ -14,7 +14,6 @@ use garethp\ews\API\Type\FolderType; use garethp\ews\API\Type\PermissionSetType; use garethp\ews\API\Type\PermissionType; -use garethp\ews\API\Type\ContactType; use PHPUnit\Framework\TestCase; class TypeConverterTest extends TestCase @@ -170,26 +169,4 @@ public function testHandlesNonStdClassData() $this->assertCount(1, $headers, 'Expected 1 header to be present'); $this->assertSame($properHeader, $headers[0], 'Expected header to match the original InternetHeaderType instance'); } - - public function testConvertStdClassToArrayForUnionType() - { - $contact = new ContactType(); - - // Simulate SOAP response with EmailAddresses as stdClass - $soapData = (object)[ - 'Name' => 'test name', - 'Email' => 'test@example.com' - ]; - - // This should convert stdClass to array for array|string type - $contact->emailAddresses = $soapData; - - $emails = $contact->getEmailAddresses(); - - $this->assertIsArray($emails, 'Expected emailAddresses to be converted to array'); - $this->assertArrayHasKey('Name', $emails, 'Expected Name property in converted array'); - $this->assertArrayHasKey('Email', $emails, 'Expected Email property in converted array'); - $this->assertEquals('test name', $emails['Name'], 'Expected Name value to match'); - $this->assertEquals('test@example.com', $emails['Email'], 'Expected Email value to match'); - } } diff --git a/tests/src/Calendar/CalendarTest.php b/tests/src/Calendar/CalendarTest.php index 80a05bda..e8439a71 100644 --- a/tests/src/Calendar/CalendarTest.php +++ b/tests/src/Calendar/CalendarTest.php @@ -251,4 +251,42 @@ public function testRecurringEventSupport() $this->markTestSkipped('Recurrence not supported on this Exchange server: ' . $e->getMessage()); } } + + /** + * Test that Categories field on CalendarItem properly handles union type of array|string + */ + public function testGetCalendarItemWithCategories() + { + $client = $this->getClient(); + + // Create a calendar item with Categories + $start = new \DateTime('2026-04-09 10:00:00'); + $end = new \DateTime('2026-04-09 11:00:00'); + + $itemIds = $client->createCalendarItems([ + 'Subject' => 'Union Type Test Event', + 'Start' => $start->format('c'), + 'End' => $end->format('c'), + 'Categories' => ['TestCategory', 'UnionTypeTest'] + ]); + + $this->assertNotEmpty($itemIds, 'Should create calendar item with Categories'); + + $itemId = $itemIds[0]; + + $queriedEvent = $client->getItem($itemId, [ + 'ItemShape' => [ + 'BodyType' => 'Text', + ], + ]); + + $this->assertNotNull($queriedEvent, 'Should retrieve calendar item with Categories'); + + $categories = $queriedEvent->getCategories(); + $this->assertNotInstanceOf(\stdClass::class, $categories, 'Categories must be array or string, not stdClass'); + $this->assertTrue(is_array($categories) || is_string($categories), 'Categories must be array or string'); + + // Clean up + $client->deleteItems($itemId, ['SendMeetingCancellations' => 'SendToNone']); + } }