scimitar 2.3.0 → 2.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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