scimitar 2.3.0 → 2.4.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -172,6 +172,7 @@ RSpec.describe Scimitar::Resources::Mixin do
172
172
  instance.work_email_address = 'foo.bar@test.com'
173
173
  instance.home_email_address = nil
174
174
  instance.work_phone_number = '+642201234567'
175
+ instance.organization = 'SOMEORG'
175
176
 
176
177
  g1 = MockGroup.create!(display_name: 'Group 1')
177
178
  g2 = MockGroup.create!(display_name: 'Group 2')
@@ -194,7 +195,12 @@ RSpec.describe Scimitar::Resources::Mixin do
194
195
  'externalId' => 'AA02984',
195
196
  'groups' => [{'display'=>g1.display_name, 'value'=>g1.id.to_s}, {'display'=>g3.display_name, 'value'=>g3.id.to_s}],
196
197
  'meta' => {'location'=>"https://test.com/mock_users/#{uuid}", 'resourceType'=>'User'},
197
- 'schemas' => ['urn:ietf:params:scim:schemas:core:2.0:User']
198
+ 'schemas' => ['urn:ietf:params:scim:schemas:core:2.0:User', 'urn:ietf:params:scim:schemas:extension:enterprise:2.0:User'],
199
+
200
+ 'urn:ietf:params:scim:schemas:extension:enterprise:2.0:User' => {
201
+ 'organization' => 'SOMEORG',
202
+ 'department' => nil
203
+ }
198
204
  })
199
205
  end
200
206
  end # "context 'with a UUID, renamed primary key column' do"
@@ -318,7 +324,9 @@ RSpec.describe Scimitar::Resources::Mixin do
318
324
  ],
319
325
 
320
326
  'meta' => {'location'=>'https://test.com/static_map_test', 'resourceType'=>'User'},
321
- 'schemas' => ['urn:ietf:params:scim:schemas:core:2.0:User']
327
+ 'schemas' => ['urn:ietf:params:scim:schemas:core:2.0:User', 'urn:ietf:params:scim:schemas:extension:enterprise:2.0:User'],
328
+
329
+ 'urn:ietf:params:scim:schemas:extension:enterprise:2.0:User' => {}
322
330
  })
323
331
  end
324
332
  end # "context 'using static mappings' do"
@@ -345,7 +353,9 @@ RSpec.describe Scimitar::Resources::Mixin do
345
353
  ],
346
354
 
347
355
  'meta' => {'location'=>'https://test.com/dynamic_map_test', 'resourceType'=>'User'},
348
- 'schemas' => ['urn:ietf:params:scim:schemas:core:2.0:User']
356
+ 'schemas' => ['urn:ietf:params:scim:schemas:core:2.0:User', 'urn:ietf:params:scim:schemas:extension:enterprise:2.0:User'],
357
+
358
+ 'urn:ietf:params:scim:schemas:extension:enterprise:2.0:User' => {}
349
359
  })
350
360
  end
351
361
  end # "context 'using dynamic lists' do"
@@ -402,7 +412,12 @@ RSpec.describe Scimitar::Resources::Mixin do
402
412
  'id' => '42', # Note, String
403
413
  'externalId' => 'AA02984',
404
414
  'meta' => {'location' => 'https://test.com/mock_users/42', 'resourceType' => 'User'},
405
- 'schemas' => ['urn:ietf:params:scim:schemas:core:2.0:User']
415
+ 'schemas' => ['urn:ietf:params:scim:schemas:core:2.0:User', 'urn:ietf:params:scim:schemas:extension:enterprise:2.0:User'],
416
+
417
+ 'urn:ietf:params:scim:schemas:extension:enterprise:2.0:User' => {
418
+ 'organization' => 'SOMEORG',
419
+ 'DEPARTMENT' => 'SOMEDPT'
420
+ }
406
421
  }
407
422
 
408
423
  hash = spec_helper_hupcase(hash) if force_upper_case
@@ -418,6 +433,8 @@ RSpec.describe Scimitar::Resources::Mixin do
418
433
  expect(instance.work_email_address).to eql('foo.bar@test.com')
419
434
  expect(instance.home_email_address).to be_nil
420
435
  expect(instance.work_phone_number ).to eql('+642201234567')
436
+ expect(instance.organization ).to eql('SOMEORG')
437
+ expect(instance.department ).to eql('SOMEDPT')
421
438
  end
422
439
 
423
440
  it 'honouring read-write lists' do
@@ -704,6 +721,21 @@ RSpec.describe Scimitar::Resources::Mixin do
704
721
  expect(scim_hash['name']['familyName']).to eql('Bar')
705
722
  end
706
723
 
724
+ it 'with schema extensions: overwrites' do
725
+ path = [ 'urn:ietf:params:scim:schemas:extension:enterprise:2.0:User', 'organization' ]
726
+ scim_hash = { 'urn:ietf:params:scim:schemas:extension:enterprise:2.0:User' => { 'organization' => 'SOMEORG' } }.with_indifferent_case_insensitive_access()
727
+
728
+ @instance.send(
729
+ :from_patch_backend!,
730
+ nature: 'add',
731
+ path: path,
732
+ value: 'OTHERORG',
733
+ altering_hash: scim_hash
734
+ )
735
+
736
+ expect(scim_hash['urn:ietf:params:scim:schemas:extension:enterprise:2.0:User']['organization' ]).to eql('OTHERORG')
737
+ end
738
+
707
739
  # For 'add', filter at end-of-path is nonsensical and not
708
740
  # supported by spec or Scimitar; we only test mid-path filters.
709
741
  #
@@ -892,6 +924,21 @@ RSpec.describe Scimitar::Resources::Mixin do
892
924
  expect(scim_hash['name']['givenName']).to eql('Baz')
893
925
  end
894
926
 
927
+ it 'with schema extensions: adds' do
928
+ path = [ 'urn:ietf:params:scim:schemas:extension:enterprise:2.0:User', 'organization' ]
929
+ scim_hash = {}.with_indifferent_case_insensitive_access()
930
+
931
+ @instance.send(
932
+ :from_patch_backend!,
933
+ nature: 'add',
934
+ path: path,
935
+ value: 'SOMEORG',
936
+ altering_hash: scim_hash
937
+ )
938
+
939
+ expect(scim_hash['urn:ietf:params:scim:schemas:extension:enterprise:2.0:User']['organization' ]).to eql('SOMEORG')
940
+ end
941
+
895
942
  context 'with filter mid-path: adds' do
896
943
  it 'by string match' do
897
944
  path = [ 'emails[type eq "work"]', 'value' ]
@@ -1230,6 +1277,595 @@ RSpec.describe Scimitar::Resources::Mixin do
1230
1277
 
1231
1278
  expect(scim_hash).to_not have_key('emails')
1232
1279
  end
1280
+
1281
+ # What we expect:
1282
+ #
1283
+ # https://tools.ietf.org/html/rfc7644#section-3.5.2.2
1284
+ # https://docs.snowflake.com/en/user-guide/scim-intro.html#patch-scim-v2-groups-id
1285
+ #
1286
+ # ...vs accounting for the unusual payloads we sometimes get,
1287
+ # tested here.
1288
+ #
1289
+ context 'special cases' do
1290
+
1291
+ # https://learn.microsoft.com/en-us/azure/active-directory/app-provisioning/use-scim-to-provision-users-and-groups#update-group-remove-members
1292
+ #
1293
+ context 'Microsoft-style payload' do
1294
+ context 'removing a user from a group' do
1295
+ it 'removes identified user' do
1296
+ path = [ 'members' ]
1297
+ value = [ { '$ref' => nil, 'value' => 'f648f8d5ea4e4cd38e9c' } ]
1298
+ scim_hash = {
1299
+ 'displayname' => 'Mock group',
1300
+ 'members' => [
1301
+ {
1302
+ 'value' => '50ca93d04ab0c2de4772',
1303
+ 'display' => 'Ingrid Smith',
1304
+ 'type' => 'User'
1305
+ },
1306
+ {
1307
+ 'value' => 'f648f8d5ea4e4cd38e9c',
1308
+ 'display' => 'Fred Smith',
1309
+ 'type' => 'User'
1310
+ },
1311
+ {
1312
+ 'value' => 'a774d480e8112101375b',
1313
+ 'display' => 'Taylor Smith',
1314
+ 'type' => 'User'
1315
+ }
1316
+ ]
1317
+ }.with_indifferent_case_insensitive_access()
1318
+
1319
+ @instance.send(
1320
+ :from_patch_backend!,
1321
+ nature: 'remove',
1322
+ path: path,
1323
+ value: value,
1324
+ altering_hash: scim_hash
1325
+ )
1326
+
1327
+ expect(scim_hash).to eql({
1328
+ 'displayname' => 'Mock group',
1329
+ 'members' => [
1330
+ {
1331
+ 'value' => '50ca93d04ab0c2de4772',
1332
+ 'display' => 'Ingrid Smith',
1333
+ 'type' => 'User'
1334
+ },
1335
+ {
1336
+ 'value' => 'a774d480e8112101375b',
1337
+ 'display' => 'Taylor Smith',
1338
+ 'type' => 'User'
1339
+ }
1340
+ ]
1341
+ })
1342
+ end
1343
+
1344
+ it 'removes multiple identified users' do
1345
+ path = [ 'members' ]
1346
+ value = [
1347
+ { '$ref' => nil, 'value' => 'f648f8d5ea4e4cd38e9c' },
1348
+ { '$ref' => nil, 'value' => '50ca93d04ab0c2de4772' }
1349
+ ]
1350
+ scim_hash = {
1351
+ 'displayname' => 'Mock group',
1352
+ 'members' => [
1353
+ {
1354
+ 'value' => '50ca93d04ab0c2de4772',
1355
+ 'display' => 'Ingrid Smith',
1356
+ 'type' => 'User'
1357
+ },
1358
+ {
1359
+ 'value' => 'f648f8d5ea4e4cd38e9c',
1360
+ 'display' => 'Fred Smith',
1361
+ 'type' => 'User'
1362
+ },
1363
+ {
1364
+ 'value' => 'a774d480e8112101375b',
1365
+ 'display' => 'Taylor Smith',
1366
+ 'type' => 'User'
1367
+ }
1368
+ ]
1369
+ }.with_indifferent_case_insensitive_access()
1370
+
1371
+ @instance.send(
1372
+ :from_patch_backend!,
1373
+ nature: 'remove',
1374
+ path: path,
1375
+ value: value,
1376
+ altering_hash: scim_hash
1377
+ )
1378
+
1379
+ expect(scim_hash).to eql({
1380
+ 'displayname' => 'Mock group',
1381
+ 'members' => [
1382
+ {
1383
+ 'value' => 'a774d480e8112101375b',
1384
+ 'display' => 'Taylor Smith',
1385
+ 'type' => 'User'
1386
+ }
1387
+ ]
1388
+ })
1389
+ end
1390
+
1391
+ it 'removes all users individually without error' do
1392
+ path = [ 'members' ]
1393
+ value = [ { '$ref' => nil, 'value' => 'f648f8d5ea4e4cd38e9c' } ]
1394
+ scim_hash = {
1395
+ 'displayname' => 'Mock group',
1396
+ 'members' => [
1397
+ {
1398
+ 'value' => 'f648f8d5ea4e4cd38e9c',
1399
+ 'display' => 'Fred Smith',
1400
+ 'type' => 'User'
1401
+ }
1402
+ ]
1403
+ }.with_indifferent_case_insensitive_access()
1404
+
1405
+ @instance.send(
1406
+ :from_patch_backend!,
1407
+ nature: 'remove',
1408
+ path: path,
1409
+ value: value,
1410
+ altering_hash: scim_hash
1411
+ )
1412
+
1413
+ expect(scim_hash).to eql({
1414
+ 'displayname' => 'Mock group',
1415
+ 'members' => []
1416
+ })
1417
+ end
1418
+
1419
+ it 'can match on multiple attributes' do
1420
+ path = [ 'members' ]
1421
+ value = [ { '$ref' => nil, 'value' => 'f648f8d5ea4e4cd38e9c', 'type' => 'User' } ]
1422
+ scim_hash = {
1423
+ 'displayname' => 'Mock group',
1424
+ 'members' => [
1425
+ {
1426
+ 'value' => 'f648f8d5ea4e4cd38e9c',
1427
+ 'display' => 'Fred Smith',
1428
+ 'type' => 'User'
1429
+ }
1430
+ ]
1431
+ }.with_indifferent_case_insensitive_access()
1432
+
1433
+ @instance.send(
1434
+ :from_patch_backend!,
1435
+ nature: 'remove',
1436
+ path: path,
1437
+ value: value,
1438
+ altering_hash: scim_hash
1439
+ )
1440
+
1441
+ expect(scim_hash).to eql({
1442
+ 'displayname' => 'Mock group',
1443
+ 'members' => []
1444
+ })
1445
+ end
1446
+
1447
+ it 'ignores unrecognised users' do
1448
+ path = [ 'members' ]
1449
+ value = [ { '$ref' => nil, 'value' => '11b054a9c85216ed9356' } ]
1450
+ scim_hash = {
1451
+ 'displayname' => 'Mock group',
1452
+ 'members' => [
1453
+ {
1454
+ 'value' => 'f648f8d5ea4e4cd38e9c',
1455
+ 'display' => 'Fred Smith',
1456
+ 'type' => 'User'
1457
+ }
1458
+ ]
1459
+ }.with_indifferent_case_insensitive_access()
1460
+
1461
+ @instance.send(
1462
+ :from_patch_backend!,
1463
+ nature: 'remove',
1464
+ path: path,
1465
+ value: value,
1466
+ altering_hash: scim_hash
1467
+ )
1468
+
1469
+ # The 'value' mismatched, so the user was not removed.
1470
+ #
1471
+ expect(scim_hash).to eql({
1472
+ 'displayname' => 'Mock group',
1473
+ 'members' => [
1474
+ {
1475
+ 'value' => 'f648f8d5ea4e4cd38e9c',
1476
+ 'display' => 'Fred Smith',
1477
+ 'type' => 'User'
1478
+ }
1479
+ ]
1480
+ })
1481
+ end
1482
+
1483
+ it 'ignores a mismatch on (for example) "type"' do
1484
+ path = [ 'members' ]
1485
+ value = [ { '$ref' => nil, 'value' => 'f648f8d5ea4e4cd38e9c', 'type' => 'Group' } ]
1486
+ scim_hash = {
1487
+ 'displayname' => 'Mock group',
1488
+ 'members' => [
1489
+ {
1490
+ 'value' => 'f648f8d5ea4e4cd38e9c',
1491
+ 'display' => 'Fred Smith',
1492
+ 'type' => 'User'
1493
+ }
1494
+ ]
1495
+ }.with_indifferent_case_insensitive_access()
1496
+
1497
+ @instance.send(
1498
+ :from_patch_backend!,
1499
+ nature: 'remove',
1500
+ path: path,
1501
+ value: value,
1502
+ altering_hash: scim_hash
1503
+ )
1504
+
1505
+ # Type 'Group' mismatches 'User', so the user was not
1506
+ # removed.
1507
+ #
1508
+ expect(scim_hash).to eql({
1509
+ 'displayname' => 'Mock group',
1510
+ 'members' => [
1511
+ {
1512
+ 'value' => 'f648f8d5ea4e4cd38e9c',
1513
+ 'display' => 'Fred Smith',
1514
+ 'type' => 'User'
1515
+ }
1516
+ ]
1517
+ })
1518
+ end
1519
+
1520
+ it 'matches keys case-insensitive' do
1521
+ path = [ 'members' ]
1522
+ value = [ { '$ref' => nil, 'VALUe' => 'f648f8d5ea4e4cd38e9c' } ]
1523
+ scim_hash = {
1524
+ 'displayname' => 'Mock group',
1525
+ 'memBERS' => [
1526
+ {
1527
+ 'vaLUe' => 'f648f8d5ea4e4cd38e9c',
1528
+ 'display' => 'Fred Smith',
1529
+ 'type' => 'User'
1530
+ }
1531
+ ]
1532
+ }.with_indifferent_case_insensitive_access()
1533
+
1534
+ @instance.send(
1535
+ :from_patch_backend!,
1536
+ nature: 'remove',
1537
+ path: path,
1538
+ value: value,
1539
+ altering_hash: scim_hash
1540
+ )
1541
+
1542
+ expect(scim_hash).to eql({
1543
+ 'displayname' => 'Mock group',
1544
+ 'members' => []
1545
+ })
1546
+ end
1547
+
1548
+ it 'matches values case-sensitive' do
1549
+ path = [ 'members' ]
1550
+ value = [ { '$ref' => nil, 'value' => 'f648f8d5ea4e4cd38e9c', 'type' => 'USER' } ]
1551
+ scim_hash = {
1552
+ 'displayname' => 'Mock group',
1553
+ 'members' => [
1554
+ {
1555
+ 'value' => 'f648f8d5ea4e4cd38e9c',
1556
+ 'display' => 'Fred Smith',
1557
+ 'type' => 'User'
1558
+ }
1559
+ ]
1560
+ }.with_indifferent_case_insensitive_access()
1561
+
1562
+ @instance.send(
1563
+ :from_patch_backend!,
1564
+ nature: 'remove',
1565
+ path: path,
1566
+ value: value,
1567
+ altering_hash: scim_hash
1568
+ )
1569
+
1570
+ # USER mismatchs User, so the user was not removed.
1571
+ #
1572
+ expect(scim_hash).to eql({
1573
+ 'displayname' => 'Mock group',
1574
+ 'members' => [
1575
+ {
1576
+ 'value' => 'f648f8d5ea4e4cd38e9c',
1577
+ 'display' => 'Fred Smith',
1578
+ 'type' => 'User'
1579
+ }
1580
+ ]
1581
+ })
1582
+ end
1583
+ end # "context 'removing a user from a group' do"
1584
+
1585
+ context 'generic use' do
1586
+ it 'removes matched items' do
1587
+ path = [ 'emails' ]
1588
+ value = [ { 'type' => 'work' } ]
1589
+ scim_hash = {
1590
+ 'emails' => [
1591
+ {
1592
+ 'type' => 'home',
1593
+ 'value' => 'home@test.com'
1594
+ },
1595
+ {
1596
+ 'type' => 'work',
1597
+ 'value' => 'work@test.com'
1598
+ }
1599
+ ]
1600
+ }.with_indifferent_case_insensitive_access()
1601
+
1602
+ @instance.send(
1603
+ :from_patch_backend!,
1604
+ nature: 'remove',
1605
+ path: path,
1606
+ value: value,
1607
+ altering_hash: scim_hash
1608
+ )
1609
+
1610
+ expect(scim_hash).to eql({
1611
+ 'emails' => [
1612
+ {
1613
+ 'type' => 'home',
1614
+ 'value' => 'home@test.com'
1615
+ }
1616
+ ]
1617
+ })
1618
+ end
1619
+
1620
+ it 'ignores unmatched items' do
1621
+ path = [ 'emails' ]
1622
+ value = [ { 'type' => 'missing' } ]
1623
+ scim_hash = {
1624
+ 'emails' => [
1625
+ {
1626
+ 'type' => 'home',
1627
+ 'value' => 'home@test.com'
1628
+ },
1629
+ {
1630
+ 'type' => 'work',
1631
+ 'value' => 'work@test.com'
1632
+ }
1633
+ ]
1634
+ }.with_indifferent_case_insensitive_access()
1635
+
1636
+ @instance.send(
1637
+ :from_patch_backend!,
1638
+ nature: 'remove',
1639
+ path: path,
1640
+ value: value,
1641
+ altering_hash: scim_hash
1642
+ )
1643
+
1644
+ expect(scim_hash).to eql({
1645
+ 'emails' => [
1646
+ {
1647
+ 'type' => 'home',
1648
+ 'value' => 'home@test.com'
1649
+ },
1650
+ {
1651
+ 'type' => 'work',
1652
+ 'value' => 'work@test.com'
1653
+ }
1654
+ ]
1655
+ })
1656
+ end
1657
+
1658
+ it 'compares string forms' do
1659
+ path = [ 'test' ]
1660
+ value = [
1661
+ { 'active' => true, 'value' => '12' },
1662
+ { 'active' => 'false', 'value' => 42 }
1663
+ ]
1664
+ scim_hash = {
1665
+ 'test' => [
1666
+ {
1667
+ 'active' => 'true',
1668
+ 'value' => 12
1669
+ },
1670
+ {
1671
+ 'active' => false,
1672
+ 'value' => '42'
1673
+ }
1674
+ ]
1675
+ }.with_indifferent_case_insensitive_access()
1676
+
1677
+ @instance.send(
1678
+ :from_patch_backend!,
1679
+ nature: 'remove',
1680
+ path: path,
1681
+ value: value,
1682
+ altering_hash: scim_hash
1683
+ )
1684
+
1685
+ expect(scim_hash).to eql({'test' => []})
1686
+ end
1687
+
1688
+ it 'handles a singular to-remove value rather than an array' do
1689
+ path = [ 'emails' ]
1690
+ value = { 'type' => 'work' }
1691
+ scim_hash = {
1692
+ 'emails' => [
1693
+ {
1694
+ 'type' => 'home',
1695
+ 'value' => 'home@test.com'
1696
+ },
1697
+ {
1698
+ 'type' => 'work',
1699
+ 'value' => 'work@test.com'
1700
+ }
1701
+ ]
1702
+ }.with_indifferent_case_insensitive_access()
1703
+
1704
+ @instance.send(
1705
+ :from_patch_backend!,
1706
+ nature: 'remove',
1707
+ path: path,
1708
+ value: value,
1709
+ altering_hash: scim_hash
1710
+ )
1711
+
1712
+ expect(scim_hash).to eql({
1713
+ 'emails' => [
1714
+ {
1715
+ 'type' => 'home',
1716
+ 'value' => 'home@test.com'
1717
+ }
1718
+ ]
1719
+ })
1720
+ end
1721
+
1722
+ it 'handles simple values rather than object (Hash) values' do
1723
+ path = [ 'test' ]
1724
+ value = 42
1725
+ scim_hash = {
1726
+ 'test' => [
1727
+ '21',
1728
+ '42',
1729
+ '15'
1730
+ ]
1731
+ }.with_indifferent_case_insensitive_access()
1732
+
1733
+ @instance.send(
1734
+ :from_patch_backend!,
1735
+ nature: 'remove',
1736
+ path: path,
1737
+ value: value,
1738
+ altering_hash: scim_hash
1739
+ )
1740
+
1741
+ expect(scim_hash).to eql({
1742
+ 'test' => [
1743
+ '21',
1744
+ '15'
1745
+ ]
1746
+ })
1747
+ end
1748
+ end
1749
+ end # "context 'Microsoft-style payload' do"
1750
+
1751
+ # https://help.salesforce.com/s/articleView?id=sf.identity_scim_manage_groups.htm&type=5
1752
+ #
1753
+ context 'Salesforce-style payload' do
1754
+ it 'removes identified user' do
1755
+ path = [ 'members' ]
1756
+ value = { 'members' => [ { '$ref' => nil, 'value' => 'f648f8d5ea4e4cd38e9c' } ] }
1757
+ scim_hash = {
1758
+ 'displayname' => 'Mock group',
1759
+ 'members' => [
1760
+ {
1761
+ 'value' => '50ca93d04ab0c2de4772',
1762
+ 'display' => 'Ingrid Smith',
1763
+ 'type' => 'User'
1764
+ },
1765
+ {
1766
+ 'value' => 'f648f8d5ea4e4cd38e9c',
1767
+ 'display' => 'Fred Smith',
1768
+ 'type' => 'User'
1769
+ }
1770
+ ]
1771
+ }.with_indifferent_case_insensitive_access()
1772
+
1773
+ @instance.send(
1774
+ :from_patch_backend!,
1775
+ nature: 'remove',
1776
+ path: path,
1777
+ value: value,
1778
+ altering_hash: scim_hash
1779
+ )
1780
+
1781
+ expect(scim_hash).to eql({
1782
+ 'displayname' => 'Mock group',
1783
+ 'members' => [
1784
+ {
1785
+ 'value' => '50ca93d04ab0c2de4772',
1786
+ 'display' => 'Ingrid Smith',
1787
+ 'type' => 'User'
1788
+ }
1789
+ ]
1790
+ })
1791
+ end
1792
+
1793
+ it 'matches the "members" key case-insensitive' do
1794
+ path = [ 'members' ]
1795
+ value = { 'MEMBERS' => [ { '$ref' => nil, 'value' => 'f648f8d5ea4e4cd38e9c' } ] }
1796
+ scim_hash = {
1797
+ 'displayname' => 'Mock group',
1798
+ 'members' => [
1799
+ {
1800
+ 'value' => 'f648f8d5ea4e4cd38e9c',
1801
+ 'display' => 'Fred Smith',
1802
+ 'type' => 'User'
1803
+ },
1804
+ {
1805
+ 'value' => 'a774d480e8112101375b',
1806
+ 'display' => 'Taylor Smith',
1807
+ 'type' => 'User'
1808
+ }
1809
+ ]
1810
+ }.with_indifferent_case_insensitive_access()
1811
+
1812
+ @instance.send(
1813
+ :from_patch_backend!,
1814
+ nature: 'remove',
1815
+ path: path,
1816
+ value: value,
1817
+ altering_hash: scim_hash
1818
+ )
1819
+
1820
+ expect(scim_hash).to eql({
1821
+ 'displayname' => 'Mock group',
1822
+ 'members' => [
1823
+ {
1824
+ 'value' => 'a774d480e8112101375b',
1825
+ 'display' => 'Taylor Smith',
1826
+ 'type' => 'User'
1827
+ }
1828
+ ]
1829
+ })
1830
+ end
1831
+
1832
+ it 'ignores unrecognised users' do
1833
+ path = [ 'members' ]
1834
+ value = { 'members' => [ { '$ref' => nil, 'value' => '11b054a9c85216ed9356' } ] }
1835
+ scim_hash = {
1836
+ 'displayname' => 'Mock group',
1837
+ 'members' => [
1838
+ {
1839
+ 'value' => 'f648f8d5ea4e4cd38e9c',
1840
+ 'display' => 'Fred Smith',
1841
+ 'type' => 'User'
1842
+ }
1843
+ ]
1844
+ }.with_indifferent_case_insensitive_access()
1845
+
1846
+ @instance.send(
1847
+ :from_patch_backend!,
1848
+ nature: 'remove',
1849
+ path: path,
1850
+ value: value,
1851
+ altering_hash: scim_hash
1852
+ )
1853
+
1854
+ # The 'value' mismatched, so the user was not removed.
1855
+ #
1856
+ expect(scim_hash).to eql({
1857
+ 'displayname' => 'Mock group',
1858
+ 'members' => [
1859
+ {
1860
+ 'value' => 'f648f8d5ea4e4cd38e9c',
1861
+ 'display' => 'Fred Smith',
1862
+ 'type' => 'User'
1863
+ }
1864
+ ]
1865
+ })
1866
+ end
1867
+ end # "context 'Salesforce-style payload' do"
1868
+ end # "context 'special cases' do"
1233
1869
  end # context 'when prior value already exists' do
1234
1870
 
1235
1871
  context 'when value is not present' do
@@ -1954,7 +2590,7 @@ RSpec.describe Scimitar::Resources::Mixin do
1954
2590
  :from_patch_backend!,
1955
2591
  nature: 'remove',
1956
2592
  path: ['complex[type eq "type1"]', 'data', 'nested[nature eq "nature2"]', 'info'],
1957
- value: [{ 'deeper' => 'addition' }],
2593
+ value: nil,
1958
2594
  altering_hash: scim_hash
1959
2595
  )
1960
2596
 
@@ -2091,6 +2727,38 @@ RSpec.describe Scimitar::Resources::Mixin do
2091
2727
  expect(@instance.first_name).to eql('Baz')
2092
2728
  end
2093
2729
 
2730
+ # Note odd ":" separating schema ID from first attribute, although
2731
+ # the nature of JSON rendering / other payloads might lead you to
2732
+ # expect a "." as with any other path component.
2733
+ #
2734
+ # Note the ":" separating the schema ID (URN) from the attribute.
2735
+ # The nature of JSON rendering / other payloads might lead you to
2736
+ # expect a "." as with any complex types, but that's not the case;
2737
+ # see https://tools.ietf.org/html/rfc7644#section-3.10, or
2738
+ # https://tools.ietf.org/html/rfc7644#section-3.5.2 of which in
2739
+ # particular, https://tools.ietf.org/html/rfc7644#page-35.
2740
+ #
2741
+ it 'which updates attributes defined by extension schema' do
2742
+ @instance.update!(department: 'SOMEDPT')
2743
+
2744
+ path = 'urn:ietf:params:scim:schemas:extension:enterprise:2.0:User:department'
2745
+ path = path.upcase if force_upper_case
2746
+
2747
+ patch = {
2748
+ 'schemas' => ['urn:ietf:params:scim:api:messages:2.0:PatchOp'],
2749
+ 'Operations' => [
2750
+ {
2751
+ 'op' => 'replace',
2752
+ 'path' => path,
2753
+ 'value' => 'OTHERDPT'
2754
+ }
2755
+ ]
2756
+ }
2757
+
2758
+ @instance.from_scim_patch!(patch_hash: patch)
2759
+ expect(@instance.department).to eql('OTHERDPT')
2760
+ end
2761
+
2094
2762
  it 'which updates with filter match' do
2095
2763
  @instance.update!(work_email_address: 'work@test.com', home_email_address: 'home@test.com')
2096
2764