--[[----------------------------------------------------------------------------

WFCatSpaceFeedsManagerTest.lua

--------------------------------------------------------------------------------

ADOBE CONFIDENTIAL
------------------
Copyright 2019 Adobe
All Rights Reserved.

NOTICE: All information contained herein is, and remains
the property of Adobe and its suppliers, if any. The intellectual
and technical concepts contained herein are proprietary to Adobe
and its suppliers and are protected by all applicable intellectual
property laws, including trade secret and copyright laws.
Dissemination of this information or reproduction of this material
is strictly forbidden unless prior written permission is obtained
from Adobe.

------------------------------------------------------------------------------]]

	-- WF.core.self_test
local WFRxTestCase = require 'WFRxTestCase'

	-- WF.core.utils
local KSDate = require 'KSDate'
local KSIdGenerator = require 'KSIdGenerator'
local KSTableUtils = require 'KSTableUtils'
local WFPlatformInfo = require 'WFPlatformInfo'
local WFRxBatchedBulkApplyOverlay = require 'WFRxBatchedBulkApplyOverlay'
local WFFileUtils = require 'WFFileUtils'

	-- WF.managed_catalog
local WFCatAssetTestUtils = require 'WFCatAssetTestUtils'
local WFCatAssetUtils = require 'WFCatAssetUtils'
local WFCatErrorHandler = require 'WFCatErrorHandler'
local WFCatMetadataPuller = require 'WFCatMetadataPuller'
local WFCatMockCatalog = require 'WFCatMockCatalog'
local WFCatSpaceFeedsManager = require 'WFCatSpaceFeedsManager'
local WFCatOzTestSession = require 'WFCatOzTestSession'
local WFCatSpecMock = require 'WFCatSpecMock'
local WFCatSharedWithYouUtils = require 'WFCatSharedWithYouUtils'
local WFCatUtils = require 'WFCatUtils'

	-- WF.web_client
local KSJsonEncoder = require 'KSJsonEncoder'
local KSJsonDecoder = require 'KSJsonDecoder'
local WFRxHttpClient = require 'WFRxHttpClient'

--------------------------------------------------------------------------------

local kUserId = KSIdGenerator.uuidInLowercaseHex()

--------------------------------------------------------------------------------

local catalogId = '98db98bfd554470dab2f862a4b853c40'

local catalogLink = "/v2/catalogs/" .. catalogId

local startingFeedLink = catalogLink .. "/updates"

local actualNextUpdateFeedLink = startingFeedLink .. '?updated_since=pull1'

--------------------------------------------------------------------------------

local function bulkDelete( catalog, docIds )

	-- very simple bulkDelete that does not delete album_assets automatically

	local overlays = {}

	for i, docId in ipairs( docIds ) do
		overlays[ #overlays + 1 ] = {
			_id = docId,
			_deleted = true,
			_localOnly = { deletedLocally = true },
		}
	end
	
	return WFRxBatchedBulkApplyOverlay( "bulkDeleteTest", catalog:docStore(), overlays )
	
end

--------------------------------------------------------------------------------

local function deleteDoc( catalog, docId )

	return bulkDelete( catalog, { docId } )
end

--------------------------------------------------------------------------------

local function getActualNextUpdateSpaceFeedLink( spaceId )

	local spaceLink = "/v2/spaces/" .. spaceId

	local startingSpaceFeedLink = spaceLink .. "/updates"

	return startingSpaceFeedLink .. '?updated_since=pull1'

end

--------------------------------------------------------------------------------

local function buildDocValue( rxTest, wfCatalog, assetId, overrides )

	assetId = assetId or KSIdGenerator.uuidInLowercaseHex()
	
	return WFCatAssetTestUtils.createAssetDocument( rxTest, wfCatalog, assetId, overrides )

end

--------------------------------------------------------------------------------

local WFCatMockCatalog_createWFCatalogForTestWithSpec = assert( WFCatMockCatalog.createWFCatalogForTestWithSpec )

---------------------------------------------------------------------------------

local function createWFCatalogForTestWithSpec( rxTest, wfCatalogSpec )

	local indexSpecScript

	if WFPlatformInfo.isMobile() then
		indexSpecScript = 'LrmCatalogIndexDefinitions.lua'
	end

	return WFCatMockCatalog_createWFCatalogForTestWithSpec( rxTest, wfCatalogSpec, nil, indexSpecScript )

end
--------------------------------------------------------------------------------

local function albumAssetsForFilteredAssetsQueryFn( catalog, assetIds )

	if WFPlatformInfo.isMobile() then

		return catalog:indexStore():queryOnce {
					sql = [[SELECT album_asset.docId, album_asset.assetId FROM CARRAY( ?, ?, 'char*' ) as ai JOIN album_asset ON 
								ai.value = album_asset.assetId]],
					assetIds,
					#assetIds,
			}
			
	else
	
		return catalog:indexStore():queryOnce {
				sql = [[SELECT album_asset_v2.docId, album_asset_v2.assetId FROM CARRAY( ?, ?, 'char*' ) as ai JOIN album_asset_v2 ON 
							ai.value = album_asset_v2.assetId]],
				assetIds,
				#assetIds,
			}
	
	end
		
end

--------------------------------------------------------------------------------

local function verifyLogData( rxTest, actualLogData, expectedLogData, expectedUpdateFeedLink, maybeSpaceUserId )

	local nextUpdateFeedLink = actualLogData.nextUpdateFeedLink
	actualLogData.nextUpdateFeedLink = nil
	actualLogData.isOzCatalogDeleted = nil
	actualLogData.lastUpdateCycleWithData = nil
	actualLogData.isCurrentlySyncing = nil
	
	if maybeSpaceUserId then
		expectedUpdateFeedLink = expectedUpdateFeedLink or getActualNextUpdateSpaceFeedLink( maybeSpaceUserId )
	else
		expectedUpdateFeedLink = expectedUpdateFeedLink or actualNextUpdateFeedLink
	end
	
	rxTest:expectEqual( expectedLogData, actualLogData )

	local nextUpdateFeedLinkDocId = 'Oz-nextUpdateFeedLink'
	
	if maybeSpaceUserId then
		nextUpdateFeedLinkDocId = string.format( "%s--Space_%s_Puller", nextUpdateFeedLinkDocId, maybeSpaceUserId )
	end

	if expectedUpdateFeedLink == 'empty' then
	
		rxTest:expectSingleValue( nextUpdateFeedLink, {
			_empty = true,
			_id = nextUpdateFeedLinkDocId
		} )
	
	elseif expectedUpdateFeedLink ~= 'ignore' then

		rxTest:expectSingleValue( nextUpdateFeedLink, {
			link = expectedUpdateFeedLink,
			_id = nextUpdateFeedLinkDocId
		} )
	
	end
	

end

--------------------------------------------------------------------------------

local function createCatalogSpecForTest( rxTest, space1, space2, additionalResponses, optArgs )

	optArgs = optArgs or {}
	
	local spacesAreOverQuota = optArgs.spacesAreOverQuota
	local entitlementExpired = optArgs.entitlementExpired
	local maintenanceLock = optArgs.maintenanceLock
	local spaceUser1Role = optArgs.spaceUser1Role
	local spaceUser2Role = optArgs.spaceUser2Role
	
	local expectedResponses = {}
	
	-- Feed-Of-Feeds Response.
	
	local space1FeedOfFeedsData = ""
	local space2FeedOfFeedsData = ""
	
	if space1 then
	
		space1FeedOfFeedsData = [[{
				  "created": "2019-08-21T17:01:02.467393Z",
				  "type": "space",
				  "subtype": "shared",
				  "id": "<space1_spaceId>",
				  "owner": {
					"id": "<space1_spaceOwnerId>",
					"outOfQuota": false,
					"entitlementExpired": false,
					"maintenanceLock": false,
					"links": {
					  "self": {
						"href": "spaces/<space1_spaceId>/updates/<space1_spaceOwnerId>"
					  }
					}
				  },
				  "this_space_user": {
					"id": "<space1_youSpaceUserId>",
					"user": {
					  "id": "<space1_youFeedbackUserId>",
					  "links": {
						"self": {
						  "href": "spaces/<space1_spaceId>/updates/<space1_youFeedbackUserId>"
						}
					  }
					},
					"links": {
					  "self": {
						"href": "spaces/<space1_spaceId>/updates/<space1_youSpaceUserId>"
					  }
					}
				  },
				  "albums": [
					{
					  "id": "<album1_id>",
					  "links": {
						"self": {
						  "href": "spaces/<space1_spaceId>/updates/<album1_id>"
						}
					  },
					  "space_album": {
						"id": "<spaceAlbum1_id>",
						"links": {
						  "self": {
							"href": "spaces/<space1_spaceId>/updates/<spaceAlbum1_id>"
						  }
						}
					  }
					}
				  ],
				  "revision_ids": [
					"33ca44a7da51f56e6d3f9e80b9181aaf",
					"813254e958ea3ffac99d32cda7acc969",
					"b6088deb146acbbde9268c50156fa19a",
					"a0cb906593fa65713d198cec5407426a",
					"3a7c6f13acb6fd7afa9d979681ae1e20",
					"8f8b861e17b67454062ab16b7437b189",
					"2ddfea16ac1a4af1cd0b9a1937fc9c3a",
					"d86b4baa17e22e23bf1a87e84e7d9807"
				  ],
				  "updated": "2019-08-21T17:01:08.471614Z",
				  "country_blocked": false,
				  "payload": {
					"userUpdated": "2019-08-21T17:01:08.445000Z",
					"allowInviteRequests": true,
					"userCreated": "2019-08-21T17:01:02.439000Z",
					"presentationFilters": true,
					"location": true,
					"download": true,
					"private": true,
					"metadata": true
				  },
				  "createdOnClient": "Chrome-76.0.3809-MacOSX",
				  "links": {
					"self": {
					  "href": "spaces/<space1_spaceId>/updates/<space1_spaceId>"
					},
					"current": {
					  "href": "spaces/<space1_spaceId>/updates?updated_since=pull1"
					},
					"/rels/web_space": {
					  "href": "http://bit.ly/2ZkCLDo"
					},
					"start": {
					  "href": "spaces/<space1_spaceId>/updates"
					}
				  }
				}]]
		
	end
	
	if space2 then
	
		space2FeedOfFeedsData = [[,
				{
				  "created": "2019-08-21T17:01:02.467393Z",
				  "type": "space",
				  "subtype": "shared",
				  "id": "<space2_spaceId>",
				  "owner": {
					"id": "<space2_spaceOwnerId>",
					"outOfQuota": false,
					"entitlementExpired": false,
					"maintenanceLock": false,
					"links": {
					  "self": {
						"href": "spaces/<space2_spaceId>/updates/<space2_spaceOwnerId>"
					  }
					}
				  },
				  "this_space_user": {
					"id": "<space2_youSpaceUserId>",
					"user": {
					  "id": "<space2_youFeedbackUserId>",
					  "links": {
						"self": {
						  "href": "spaces/<space2_spaceId>/updates/<space2_youFeedbackUserId>"
						}
					  }
					},
					"links": {
					  "self": {
						"href": "spaces/<space2_spaceId>/updates/<space2_youSpaceUserId>"
					  }
					}
				  },
				  "albums": [
					{
					  "id": "<album2_id>",
					  "links": {
						"self": {
						  "href": "spaces/<space2_spaceId>/updates/<album2_id>"
						}
					  },
					  "space_album": {
						"id": "<spaceAlbum2_id>",
						"links": {
						  "self": {
							"href": "spaces/<space2_spaceId>/updates/<spaceAlbum2_id>"
						  }
						}
					  }
					}
				  ],
				  "revision_ids": [
					"33ca4452f351f56e6d3f9e80b9181aaf"
				  ],
				  "updated": "2019-08-21T17:01:08.471614Z",
				  "country_blocked": false,
				  "payload": {
					"userUpdated": "2019-08-21T17:01:08.445000Z",
					"allowInviteRequests": true,
					"userCreated": "2019-08-21T17:01:02.439000Z",
					"presentationFilters": true,
					"location": false,
					"download": false,
					"private": false,
					"metadata": false
				  },
				  "createdOnClient": "Chrome-76.0.3809-MacOSX",
				  "links": {
					"self": {
					  "href": "spaces/<space2_spaceId>/updates/<space2_spaceId>"
					},
					"current": {
					  "href": "spaces/<space2_spaceId>/updates?updated_since=pull1"
					},
					"/rels/web_space": {
					  "href": "http://bit.ly/2ZkCLDo"
					},
					"start": {
					  "href": "spaces/<space2_spaceId>/updates"
					}
				  }
				}]]
				
	end
	
	expectedResponses[ #expectedResponses + 1 ] = 
		{
		
			url = "/v2/spaces/updates",
			
			response = string.format( [[{
			  "base": "https://photos-stage.adobe.io/v2/",
			  "updates": [
				%s%s
			  ],
			  "links": {
				"next": {
				  "href": "/v2/spaces/updates?updated_since=pull1"
				}
			  }
			}]], space1FeedOfFeedsData, space2FeedOfFeedsData ),
			
			responseFilter = function( response )

				if space1 then
					response = response:gsub( '<space1_spaceId>', space1.id )
					response = response:gsub( '<space1_youSpaceUserId>', space1.youSpaceUserId )
					response = response:gsub( '<space1_spaceOwnerId>', space1.ownerId or KSIdGenerator.uuidInLowercaseHex() )
					response = response:gsub( '<space1_youFeedbackUserId>', space1.youFeedbackUserId or KSIdGenerator.uuidInLowercaseHex() ) 
					
					response = response:gsub( '<album1_id>', space1.albumId )
					response = response:gsub( '<spaceAlbum1_id>', space1.spaceAlbumId )
				end
				
				if space2 then
					response = response:gsub( '<space2_spaceId>', space2.id )
					response = response:gsub( '<space2_youSpaceUserId>', space2.youSpaceUserId )
					response = response:gsub( '<space2_spaceOwnerId>', space2.ownerId or KSIdGenerator.uuidInLowercaseHex() )
					response = response:gsub( '<space2_youFeedbackUserId>', space2.youFeedbackUserId or KSIdGenerator.uuidInLowercaseHex() ) 
					
					response = response:gsub( '<album2_id>', space2.albumId )
					response = response:gsub( '<spaceAlbum2_id>', space2.spaceAlbumId )
				end
				
				if entitlementExpired then
					response = response:gsub( '"entitlementExpired": false', '"entitlementExpired": true' ) 
						-- Inject the entitlementExpired flag right after the outOfQuota flag.
				end
				
				if maintenanceLock then
					response = response:gsub( '"maintenanceLock": false', '"maintenanceLock": true' ) 
						-- Inject the entitlementExpired flag right after the outOfQuota flag.
				end
				
				if spacesAreOverQuota then
					response = response:gsub( '"outOfQuota": false', '"outOfQuota": true' ) 
				end
				
				return response

			end,
			
			headers = {
				status_code = '200',
			},
			
			options = {
				bypassRateLimiter = true,
			}
			
		}

	-- Fetch Space User
	
	if space1 then
	
		expectedResponses[ #expectedResponses + 1 ] = 
			{
			
				url = string.format( "/v2/spaces/%s/updates/%s", space1.id, space1.youSpaceUserId ),
				
				response = [[{
					  "base": "https://photos.adobe.io/v2/spaces/<space1_spaceId>/",
					  "updated": "2019-07-25T22:27:41.466149Z",
					  "type": "space_user",
					  "payload": {
						"status": "accepted",
						"role": "contributor",
						"userUpdated": "2019-07-25T22:27:41.299863Z",
						"userCreated": "2019-07-25T22:27:41.299863Z"
					  },
					  "id": "<space1_youSpaceUserId>",
					  "user": {
						"id": "87bd59dd4c01e4f71155d7bfb50ccdf3"
					  },
					  "created": "2019-07-25T22:27:41.466149Z",
					  "revision_ids": [
						"ae43e159e80148fb47a8fe3ec4e6a718"
					  ]
					}]],
				
				responseFilter = function( response )

					response = response:gsub( '<space1_spaceId>', space1.id )
					response = response:gsub( '<space1_youSpaceUserId>', space1.youSpaceUserId )

					if spaceUser1Role then
						response = response:gsub( '"role": "contributor"', string.format( '"role": "%s"', spaceUser1Role ) ) 
					end

					return response

				end,

				headers = {
					status_code = '200',
				},
				
				options = {
					bypassRateLimiter = true,
				}
				
			}
		
	end
	
	-- Fetch Space User
	
	if space2 then
	
		expectedResponses[ #expectedResponses + 1 ] = 
			{
			
				url = string.format( "/v2/spaces/%s/updates/%s", space2.id, space2.youSpaceUserId ),
				
				response = [[{
					  "base": "https://photos.adobe.io/v2/spaces/<space2_spaceId>/",
					  "updated": "2019-07-25T22:27:41.466149Z",
					  "type": "space_user",
					  "payload": {
						"status": "accepted",
						"role": "contributor",
						"userUpdated": "2019-07-25T22:27:41.299863Z",
						"userCreated": "2019-07-25T22:27:41.299863Z"
					  },
					  "id": "<space2_youSpaceUserId>",
					  "user": {
						"id": "87bd59dd4c01e4f71155d7bfb50ccdf3"
					  },
					  "created": "2019-07-25T22:27:41.466149Z",
					  "revision_ids": [
						"ae43e159e80148fb47a8fe3ec4e6a718"
					  ]
					}]],

				responseFilter = function( response )

					response = response:gsub( '<space2_spaceId>', space2.id )
					response = response:gsub( '<space2_youSpaceUserId>', space2.youSpaceUserId )
					
					if spaceUser2Role then
						response = response:gsub( '"role": "contributor"', string.format( '"role": "%s"', spaceUser2Role ) ) 
					end
					
					return response

				end,

				headers = {
					status_code = '200',
				},
				
				options = {
					bypassRateLimiter = true,
				}
				
			}
	
	end
	
	if space1 then
	
		expectedResponses[ #expectedResponses + 1 ] = 
			{
				url = string.format( "/v2/spaces/%s/updates/%s", space1.id, space1.ownerId ),

				response = string.format( [[{
				  "base": "https://photos-stage.adobe.io/v2/spaces/%s/",
				  "created": "2015-12-07T18:33:17.171395Z",
				  "updated": "2019-09-06T16:02:31.723815Z",
				  "type": "feedbackuser",
				  "id": "%s",
				  "full_name": "Owner One",
				  "first_name": "Owner",
				  "last_name": "One"
				}]], space1.id, space1.ownerId ),

				headers = {
					status_code = '200',
				},

				options = {
					bypassRateLimiter = true,
				}

			}
		
		expectedResponses[ #expectedResponses + 1 ] =  
			{
				url = string.format( "/v2/spaces/%s/updates/%s", space1.id, space1.youFeedbackUserId ),

				response = string.format( [[{
				  "base": "https://photos-stage.adobe.io/v2/spaces/%s/",
				  "created": "2015-12-07T18:33:17.171395Z",
				  "updated": "2019-09-06T16:02:31.723815Z",
				  "type": "feedbackuser",
				  "id": "%s",
				  "full_name": "You One",
				  "first_name": "You",
				  "last_name": "One"
				}]], space1.id, space1.youFeedbackUserId ),

				headers = {
					status_code = '200',
				},

				options = {
					bypassRateLimiter = true,
				}

			}
			
		expectedResponses[ #expectedResponses + 1 ] = 
			{
				url = string.format( "/v2/spaces/%s/updates/%s", space1.id, space1.albumId ),

				response = string.format( [[{
					"created": "2015-07-30T22:36:15.470935Z",
					"type": "album",
					"subtype": "collection",
					"id": "%s",
					"updated": "2015-07-30T22:36:15.470935Z",
					"revision_ids": [
						"8706d10b9b0947909940a1ea744a9314"
					],
					"payload": {
						"name": "album1"
					}
				}]], space1.albumId ),

				headers = {
					status_code = '200',
				},

				options = {
					bypassRateLimiter = true,
				}

			}
		
		expectedResponses[ #expectedResponses + 1 ] = 
			{
				url = string.format( "/v2/spaces/%s/updates/%s", space1.id, space1.spaceAlbumId ),

				response = string.format( [[{
					"created": "2015-07-30T22:36:15.470935Z",
					"type": "space_album",
					"id": "%s",
					"updated": "2015-07-30T22:36:15.470935Z",
					"album": {
						"id": "%s"
					},
					"space": {
						"id": "%s"
					},
					"revision_ids": [
						"8706d10b9b0947909940a1ea744a9314"
					]
				}]], space1.spaceAlbumId, space1.albumId, space1.id ),

				headers = {
					status_code = '200',
				},

				options = {
					bypassRateLimiter = true,
				}

			}
	
	end
	
	if space2 then
		
		expectedResponses[ #expectedResponses + 1 ] = 
			{
				url = string.format( "/v2/spaces/%s/updates/%s", space2.id, space2.ownerId ),

				response = string.format( [[{
				  "base": "https://photos-stage.adobe.io/v2/spaces/%s/",
				  "created": "2015-12-07T18:33:17.171395Z",
				  "updated": "2019-09-06T16:02:31.723815Z",
				  "type": "feedbackuser",
				  "id": "%s",
				  "full_name": "Owner One",
				  "first_name": "Owner",
				  "last_name": "One"
				}]], space2.id, space2.ownerId ),

				headers = {
					status_code = '200',
				},

				options = {
					bypassRateLimiter = true,
				}

			}
			
		expectedResponses[ #expectedResponses + 1 ] = 	
			{
				url = string.format( "/v2/spaces/%s/updates/%s", space2.id, space2.youFeedbackUserId ),

				response = string.format( [[{
				  "base": "https://photos-stage.adobe.io/v2/spaces/%s/",
				  "created": "2015-12-07T18:33:17.171395Z",
				  "updated": "2019-09-06T16:02:31.723815Z",
				  "type": "feedbackuser",
				  "id": "%s",
				  "full_name": "You One",
				  "first_name": "You",
				  "last_name": "One"
				}]], space2.id, space2.youFeedbackUserId ),

				headers = {
					status_code = '200',
				},

				options = {
					bypassRateLimiter = true,
				}

			}
			
		expectedResponses[ #expectedResponses + 1 ] = 	
			{
				url = string.format( "/v2/spaces/%s/updates/%s", space2.id, space2.albumId ),

				response = string.format( [[{
					"created": "2015-07-30T22:36:15.470935Z",
					"type": "album",
					"subtype": "collection",
					"id": "%s",
					"updated": "2015-07-30T22:36:15.470935Z",
					"revision_ids": [
						"8706d10b9b0947909940a1ea744a9314"
					],
					"payload": {
						"name": "album1"
					}
				}]], space2.albumId ),

				headers = {
					status_code = '200',
				},

				options = {
					bypassRateLimiter = true,
				}

			}
			
		expectedResponses[ #expectedResponses + 1 ] = 
			{
				url = string.format( "/v2/spaces/%s/updates/%s", space2.id, space2.spaceAlbumId ),

				response = string.format( [[{
					"created": "2015-07-30T22:36:15.470935Z",
					"type": "space_album",
					"id": "%s",
					"updated": "2015-07-30T22:36:15.470935Z",
					"album": {
						"id": "%s"
					},
					"space": {
						"id": "%s"
					},
					"revision_ids": [
						"8706d10b9b0947909940a1ea744a9314"
					]
				}]], space2.spaceAlbumId, space2.albumId, space2.id ),
				
				headers = {
					status_code = '200',
				},

				options = {
					bypassRateLimiter = true,
				}

			}
		
	end

	expectedResponses = KSTableUtils.appendArray( expectedResponses, additionalResponses )

	local catalogSpec = WFCatSpecMock( catalogId, kUserId, rxTest, expectedResponses )

	return catalogSpec

end

--==============================================================================

local WFCatSpaceFeedsManagerTest = {}

local spaceId1 = KSIdGenerator.uuidInLowercaseHex()
local spaceUserId1 = KSIdGenerator.uuidInLowercaseHex()
local ownerId1 = KSIdGenerator.uuidInLowercaseHex()
local youFeedbackUserId1 = KSIdGenerator.uuidInLowercaseHex()

local albumId1 = KSIdGenerator.uuidInLowercaseHex()
local spaceAlbumId1 = KSIdGenerator.uuidInLowercaseHex()

local space1 = {
	id = spaceId1,
	youSpaceUserId = spaceUserId1,
	ownerId = ownerId1,
	youFeedbackUserId = youFeedbackUserId1,
	albumId = albumId1,
	spaceAlbumId = spaceAlbumId1
}

local spaceId2 = KSIdGenerator.uuidInLowercaseHex()
local spaceUserId2 = KSIdGenerator.uuidInLowercaseHex()
local ownerId2 = KSIdGenerator.uuidInLowercaseHex()
local youFeedbackUserId2 = KSIdGenerator.uuidInLowercaseHex()

local albumId2 = KSIdGenerator.uuidInLowercaseHex()
local spaceAlbumId2 = KSIdGenerator.uuidInLowercaseHex()

local space2 = {
	id = spaceId2,
	youSpaceUserId = spaceUserId2,
	ownerId = ownerId2,
	youFeedbackUserId = youFeedbackUserId2,
	albumId = albumId2,
	spaceAlbumId = spaceAlbumId2
}

--------------------------------------------------------------------------------

function WFCatSpaceFeedsManagerTest.rxTestBasicSpaceFeedsManager( rxTest )

	rxTest:setTimeLimit( 10 )
	
	local expectedResponses = { 
		{
	
			url = string.format( "/v2/spaces/%s/updates", spaceId1 ),

			-- somewhat minimized version of payload

			response = string.format( [[{
				"base": "https://photos.adobe.io",
				"updates": [
					{
						"id": "8462f5ba3b39fd050d3cd6b7352ba561",
						"created": "2015-05-27T00:34:56.669987Z",
						"updated": "2015-05-27T00:38:29.160369Z",
						"type": "asset",
						"subtype": "image",
						"revision_ids": [
							"a1e880f718a348b89663cf38f9f29d23",
							"cd1ab30cf8464ef2a9e736b50c0ba33d",
							"96f33a7215e04c9aa49346bbf8aae921",
							"813fdbda1b244c2d9f41008ba63eedba"
						],
						"payload": {
							"importSource": {
								"importTimestamp": "2015-05-26T17:34:23-07:00",
								"originalHeight": 1600,
								"originalWidth": 1068
							},
							"captureDate": "2015-05-17T15:50:12",
							"xmp": {
								"dc": {
									"rights": "test copyright string"
								}
							},
							"location": {
								"longitude": -93.600107,
								"latitude": 45.951733,
								"altitude": 377.9
							}
						}
					}
				],
				"links": {
					"next": {
						"href": "/v2/spaces/%s/updates?updated_since=pull1"
					}
				}
			}]], spaceId1 ),

			headers = {
				status_code = '200',
			},

			options = {
				bypassRateLimiter = true,
			}

		},

		{
			url = string.format( "/v2/spaces/%s/updates?updated_since=pull1", spaceId1 ),

			response = string.format( [[{
				"base": "https://photos.adobe.io",
				"updates": [
					{
						"id": "8462f5ba3b39fd050d3cd6b7352ba561",
						"created": "2015-05-27T00:34:56.669987Z",
						"updated": "2015-05-27T00:38:29.160369Z",
						"type": "asset",
						"subtype": "image",
						"revision_ids": [
							"a1e880f718a348b89663cf38f9f29d23",
							"cd1ab30cf8464ef2a9e736b50c0ba33d",
							"96f33a7215e04c9aa49346bbf8aae921",
							"813fdbda1b244c2d9f41008ba63eedba",
							"3a4a43dc2a9a4f1897c27f7a26e2e068"
						],
						"payload": {
							"xmp": {
								"dc": {
									"rights": "something changed"
								}
							},
							"importSource": {
								"importTimestamp": "2015-05-26T17:34:23-07:00",
								"originalHeight": 1600,
								"originalWidth": 1068
							},
							"captureDate": "2015-05-17T15:50:12"
						}
					}
				],
				"links": {
					"next": {
						"href": "/v2/spaces/%s/updates?updated_since=pull2"
					}
				}
			}]], spaceId1 ),

			headers = {
				status_code = '200',
			},

			options = {
				bypassRateLimiter = true,
			}

		},
		{
			url = string.format( "/v2/spaces/%s/updates?updated_since=pull2", spaceId1 ),

			response = string.format( [[{
				"base": "https://photos.adobe.io",
				"updates": [],
				"links": {
					"next": {
						"href": "/v2/spaces/%s/updates?updated_since=pull2"
					}
				}
			}]], spaceId1 ),

			headers = {
				status_code = '200',
			},

			options = {
				bypassRateLimiter = true,
			}

		},
		{
			url = string.format( "/v2/spaces/%s/updates", spaceId2 ),

			response = string.format( [[{
				"base": "https://photos.adobe.io",
				"updates": [],
				"links": {
					"next": {
						"href": "/v2/spaces/%s/updates?updated_since=pull1"
					}
				}
			}]], spaceId2 ),

			headers = {
				status_code = '200',
			},

			options = {
				bypassRateLimiter = true,
			}

		},
		{
			url = string.format( "/v2/spaces/%s/updates?updated_since=pull1", spaceId2 ),

			response = string.format( [[{
				"base": "https://photos.adobe.io",
				"updates": [],
				"links": {
					"next": {
						"href": "/v2/spaces/%s/updates?updated_since=pull1"
					}
				}
			}]], spaceId2 ),

			headers = {
				status_code = '200',
			},

			options = {
				bypassRateLimiter = true,
			}

		}
 
 	}
	
	local args = {
		spacesAreOverQuota = false,
		entitlementExpired = false,
	}
	
	local catalogSpec = createCatalogSpecForTest( rxTest, space1, space2, expectedResponses, args )
	
	local wfCatalog = createWFCatalogForTestWithSpec( rxTest, catalogSpec )
	
	local spaceFeedsManagerArgs = {
		taskDelays = {
			progressiveBackoff = false,
			noWorkDelay = 0.5,
			maxNoWorkDelay = 1,
			errorDelay = 1,
			maxDelay = 1,
		},
		isPrefetchEnabled = true,
		albumAssetsForFilteredAssetsQueryFn = albumAssetsForFilteredAssetsQueryFn,
		testMode = true,
	}

	local spaceFeedsManager = WFCatSpaceFeedsManager( wfCatalog, spaceFeedsManagerArgs )
	rxTest:addCloseTarget( spaceFeedsManager )
	
	local observer = rxTest:observe( spaceFeedsManager:isDownloadingMetadata() )
	
	rxTest:expectOnlyCompleted( spaceFeedsManager:_testOnly_getUpdatesOnce() )
	
	observer:expectDataWithinTime( 2, true ) -- Space 1
	observer:expectDataWithinTime( 2, false )
	
	observer:expectDataWithinTime( 2, true ) -- Space 2
	observer:expectDataWithinTime( 2, false )
	
	observer:dispose()
	
	local expectedLogData = {
		documentsReceivedByType = {
			update = {
				asset = {
					image = 2,
				},
				total = 2,
			},
		},
		successfulUpdateCycles = 3,
		totalDocumentsReceived = 2,
	}

	local puller = spaceFeedsManager._spacePullers[ spaceUserId1 ]
	
	local expectedLink = string.format( '/v2/spaces/%s/updates?updated_since=pull2', spaceId1 )
	
	verifyLogData( rxTest, puller:getDiagnosticLogData(), expectedLogData, expectedLink, spaceUserId1 )
	
	local expectedLogData2 = {
		documentsReceivedByType = {},
		successfulUpdateCycles = 1,
		totalDocumentsReceived = 0,
	}
	
	local puller2 = spaceFeedsManager._spacePullers[ spaceUserId2 ]
	
	local expectedLink2 = string.format( '/v2/spaces/%s/updates?updated_since=pull1', spaceId2 )
	
	verifyLogData( rxTest, puller2:getDiagnosticLogData(), expectedLogData2, expectedLink2, spaceUserId2 )
	 
end

--------------------------------------------------------------------------------

function WFCatSpaceFeedsManagerTest.rxTestSpaceOverQuota( rxTest )

	rxTest:setTimeLimit( 10 )

	local expectedResponses = {
		{
			url = string.format( "/v2/spaces/%s/updates", spaceId1 ),

			response = string.format( [[{
				"base": "https://photos.adobe.io",
				"updates": [ {
						"id": "8462f5ba3b39fd050d3cd6b7352ba561",
						"created": "2015-05-27T00:34:56.669987Z",
						"updated": "2015-05-27T00:38:29.160369Z",
						"type": "asset",
						"subtype": "image",
						"revision_ids": [
							"a1e880f718a348b89663cf38f9f29d23",
							"cd1ab30cf8464ef2a9e736b50c0ba33d",
							"96f33a7215e04c9aa49346bbf8aae921",
							"813fdbda1b244c2d9f41008ba63eedba"
						],
						"payload": {
							"importSource": {
								"importTimestamp": "2015-05-26T17:34:23-07:00",
								"originalHeight": 1600,
								"originalWidth": 1068
							},
							"captureDate": "2015-05-17T15:50:12",
							"xmp": {
								"dc": {
									"rights": "test copyright string"
								}
							},
							"location": {
								"longitude": -93.600107,
								"latitude": 45.951733,
								"altitude": 377.9
							}
						}
					}
				],
				"links": {
					"next": {
						"href": "/v2/spaces/%s/updates?updated_since=pull1"
					}
				}
			}]], spaceId1 ),

			headers = {
				status_code = '200',
			},

			options = {
				bypassRateLimiter = true,
			}

		},
		{
			url = string.format( "/v2/spaces/%s/updates?updated_since=pull1", spaceId1 ),

			response = string.format( [[{
				"base": "https://photos.adobe.io",
				"updates": [],
				"links": {
					"next": {
						"href": "/v2/spaces/%s/updates?updated_since=pull1"
					}
				}
			}]], spaceId1 ),

			headers = {
				status_code = '200',
			},

			options = {
				bypassRateLimiter = true,
			}

		},
		{
			url = string.format( "/v2/spaces/%s/updates", spaceId2 ),

			response = string.format( [[{
				"base": "https://photos.adobe.io",
				"updates": [ {
						"id": "2222f5ba3b39fd050d3cd6b7352ba561",
						"created": "2015-05-27T00:34:56.669987Z",
						"updated": "2015-05-27T00:38:29.160369Z",
						"type": "asset",
						"subtype": "image",
						"revision_ids": [
							"22e880f718a348b89663cf38f9f29d23",
							"331ab30cf8464ef2a9e736b50c0ba33d"
						],
						"payload": {
							"importSource": {
								"importTimestamp": "2015-05-26T17:34:23-07:00",
								"originalHeight": 1600,
								"originalWidth": 1068
							},
							"captureDate": "2015-05-17T15:50:12",
							"xmp": {
								"dc": {
									"rights": "test copyright string"
								}
							},
							"location": {
								"longitude": -93.600107,
								"latitude": 45.951733,
								"altitude": 377.9
							}
						}
					}
				],
				"links": {
					"next": {
						"href": "/v2/spaces/%s/updates?updated_since=pull1"
					}
				}
			}]], spaceId2 ),

			headers = {
				status_code = '200',
			},

			options = {
				bypassRateLimiter = true,
			}

		},
		{
			url = string.format( "/v2/spaces/%s/updates?updated_since=pull1", spaceId2 ),

			response = string.format( [[{
				"base": "https://photos.adobe.io",
				"updates": [],
				"links": {
					"next": {
						"href": "/v2/spaces/%s/updates?updated_since=pull1"
					}
				}
			}]], spaceId2 ),

			headers = {
				status_code = '200',
			},

			options = {
				bypassRateLimiter = true,
			}

		},
		{
		
			url = "/v2/spaces/updates?updated_since=pull1",
			
			response = [[{
			  "base": "https://photos-stage.adobe.io/v2/",
			  "updates": [
				{
				  "created": "2019-08-21T17:01:02.467393Z",
				  "type": "space",
				  "subtype": "shared",
				  "id": "<space1_spaceId>",
				  "owner": {
					"id": "<space1_spaceOwnerId>",
					"outOfQuota": false,
					"entitlementExpired": false,
					"links": {
					  "self": {
						"href": "spaces/<space1_spaceId>/updates/<space1_spaceOwnerId>"
					  }
					}
				  },
				  "this_space_user": {
					"id": "<space1_youSpaceUserId>",
					"user": {
					  "id": "<space1_youFeedbackUserId>",
					  "links": {
						"self": {
						  "href": "spaces/<space1_spaceId>/updates/<space1_youFeedbackUserId>"
						}
					  }
					},
					"links": {
					  "self": {
						"href": "spaces/<space1_spaceId>/updates/<space1_youSpaceUserId>"
					  }
					}
				  },
				  "albums": [
					{
					  "id": "<album1_id>",
					  "links": {
						"self": {
						  "href": "spaces/<space1_spaceId>/updates/<album1_id>"
						}
					  },
					  "space_album": {
						"id": "<spaceAlbum1_id>",
						"links": {
						  "self": {
							"href": "spaces/<space1_spaceId>/updates/<spaceAlbum1_id>"
						  }
						}
					  }
					}
				  ],
				  "revision_ids": [
					"33ca44a7da51f56e6d3f9e80b9181aaf",
					"813254e958ea3ffac99d32cda7acc969",
					"b6088deb146acbbde9268c50156fa19a",
					"a0cb906593fa65713d198cec5407426a",
					"3a7c6f13acb6fd7afa9d979681ae1e20",
					"8f8b861e17b67454062ab16b7437b189",
					"2ddfea16ac1a4af1cd0b9a1937fc9c3a",
					"d86b4baa17e22e23bf1a87e84e7d9807"
				  ],
				  "updated": "2019-08-21T17:01:08.471614Z",
				  "country_blocked": false,
				  "payload": {
					"userUpdated": "2019-08-21T17:01:08.445000Z",
					"allowInviteRequests": true,
					"userCreated": "2019-08-21T17:01:02.439000Z",
					"presentationFilters": true,
					"location": true,
					"download": true,
					"private": true,
					"metadata": true
				  },
				  "createdOnClient": "Chrome-76.0.3809-MacOSX",
				  "links": {
					"self": {
					  "href": "spaces/<space1_spaceId>/updates/<space1_spaceId>"
					},
					"current": {
					  "href": "spaces/<space1_spaceId>/updates?updated_since=pull1"
					},
					"/rels/web_space": {
					  "href": "http://bit.ly/2ZkCLDo"
					},
					"start": {
					  "href": "spaces/<space1_spaceId>/updates"
					}
				  }
				}
			  ],
			  "links": {
				"next": {
				  "href": "/v2/spaces/updates?updated_since=pull2"
				}
			  }
			}]],
			
			responseFilter = function( response )

				response = response:gsub( '<space1_spaceId>', space1.id )
				response = response:gsub( '<space1_youSpaceUserId>', space1.youSpaceUserId )
				response = response:gsub( '<space1_spaceOwnerId>', space1.ownerId or KSIdGenerator.uuidInLowercaseHex() )
				response = response:gsub( '<space1_youFeedbackUserId>', space1.youFeedbackUserId or KSIdGenerator.uuidInLowercaseHex() ) 
				
				response = response:gsub( '<album1_id>', space1.albumId )
				response = response:gsub( '<spaceAlbum1_id>', space1.spaceAlbumId )

				return response

			end,
			
			headers = {
				status_code = '200',
			},
			
			options = {
				bypassRateLimiter = true,
			}
				
		}

	}
	
	local args = {
		spacesAreOverQuota = true,
		entitlementExpired = false,
	}
	
	local catalogSpec = createCatalogSpecForTest( rxTest, space1, space2, expectedResponses, args )
	
	local wfCatalog = createWFCatalogForTestWithSpec( rxTest, catalogSpec )
	
	local spaceFeedsManagerArgs = {
		taskDelays = {
			progressiveBackoff = false,
			noWorkDelay = 0.5,
			maxNoWorkDelay = 1,
			errorDelay = 1,
			maxDelay = 1,
		},
		albumAssetsForFilteredAssetsQueryFn = albumAssetsForFilteredAssetsQueryFn,
		testMode = true
	}

	-- Add an asset to the space.

	local assetDocId = '44ab164cd874418ca566d5a0e259ac0d'

	local assetDoc = buildDocValue( rxTest, wfCatalog, assetDocId )
	assetDoc._spaceId = spaceId1
	
	rxTest:expectSingleValue( wfCatalog:docStore():setDocValue( assetDoc ), ':ignore:' )

	-- Start up the space feed manager

	local spaceFeedsManager = WFCatSpaceFeedsManager( wfCatalog, spaceFeedsManagerArgs )
	rxTest:addCloseTarget( spaceFeedsManager )
	
	-- Verify the active spaces.
	
	local activeSpacesObserver = rxTest:observe( spaceFeedsManager._activeSpacesStream )
	
	local isDownloadingObserver = rxTest:observe( spaceFeedsManager:isDownloadingMetadata() )
	
	rxTest:expectOnlyCompleted( spaceFeedsManager:_testOnly_getUpdatesOnce() )
	
	activeSpacesObserver:expectDataWithinTime( 2, spaceId1, spaceUserId1, ":ignore:", true, ":ignore:", ":ignore:" )
	activeSpacesObserver:expectDataWithinTime( 2, spaceId2, spaceUserId2, ":ignore:", true, ":ignore:", ":ignore:" )
	
	isDownloadingObserver:expectDataWithinTime( 5, true ) -- Space 1
	isDownloadingObserver:expectDataWithinTime( 5, false )
	
	isDownloadingObserver:expectDataWithinTime( 5, true ) -- Space 2
	isDownloadingObserver:expectDataWithinTime( 5, false )
	
	isDownloadingObserver:dispose()
	
	-- Verify the space is over quota.
	
	rxTest:expectEqual( true, spaceFeedsManager._spacePusher:getSpaceIsOverQuota( spaceId1 ) )
	
	-- Trigger the space metadata pusher.
	
	local sendNUpdates = rxTest:observe( spaceFeedsManager._spacePusher:_DEBUG_sendUpdatesOnce() )
	sendNUpdates:expectCompletedWithinTime( 5 )
	sendNUpdates:dispose()
	
	-- Verify the asset doc is marked as over quota.
	
	rxTest:expectSingleValue(

		wfCatalog:docStore():docValue( assetDocId )
			:map( function( docValue )
				return KSTableUtils.derefOpt( docValue, '_localOnly', 'ozError', 'subtype' )
			end ), 'StorageExceededError' )

	-- Get more updates. This should update spaceId1 as not over quota.
	
	rxTest:expectOnlyCompleted( spaceFeedsManager:_testOnly_getUpdatesOnce() )
	
	activeSpacesObserver:expectDataWithinTime( 2, spaceId1, spaceUserId1, ":ignore:", false, ":ignore:", ":ignore:" )
	
	activeSpacesObserver:dispose()
	
	-- Verify the asset doc is no longer marked as over quota.
	
	local errorObserver = rxTest:observe( wfCatalog:docStore():ongoingDocValue( assetDocId )
			:map( function( docValue )
				return KSTableUtils.derefOpt( docValue, '_localOnly', 'ozError', 'subtype' )
			end ) )
			
	errorObserver:expectDataWithinTime( 5, nil )
	
	errorObserver:dispose()
	
end

--------------------------------------------------------------------------------

function WFCatSpaceFeedsManagerTest.rxTestSyncPriority( rxTest )

	rxTest:setTimeLimit( 10 )

	local catalogSpec = createCatalogSpecForTest( rxTest, space1, space2 )

	local wfCatalog = createWFCatalogForTestWithSpec( rxTest, catalogSpec )
	
	local spaceFeedsManagerArgs = {
		testMode = true,
		taskDelays = {
			progressiveBackoff = false,
			noWorkDelay = 0.5,
			maxNoWorkDelay = 1,
			errorDelay = 1,
			maxDelay = 1,
		},
		albumAssetsForFilteredAssetsQueryFn = albumAssetsForFilteredAssetsQueryFn,
		testMode = true,
	}

	local spaceFeedsManager = WFCatSpaceFeedsManager( wfCatalog, spaceFeedsManagerArgs )
	rxTest:addCloseTarget( spaceFeedsManager )
	
	local observer = rxTest:observe( spaceFeedsManager._activeSpacesStream )
	
	rxTest:expectOnlyCompleted( spaceFeedsManager:_testOnly_getUpdatesOnce() )
	
	observer:expectDataWithinTime( 2, spaceId1, spaceUserId1, ":ignore:", false, ":ignore:", ":ignore:" )
	
	observer:dispose()
	
	local canFetchUpdatesStream = rxTest:observe( spaceFeedsManager:_testOnly_canFetchUpdates() )
	local canSendUpdatesStream = rxTest:observe( spaceFeedsManager:_testOnly_canSendUpdates() )
	
	spaceFeedsManager:reportCatalogInitialSyncIsComplete( false )
	
	canFetchUpdatesStream:expectDataWithinTime( 3, false )
	canSendUpdatesStream:expectDataWithinTime( 3, false )
	
	spaceFeedsManager:startFetchingUpdates()
	
	canFetchUpdatesStream:expectDataWithinTime( 3, false )
	canSendUpdatesStream:expectDataWithinTime( 3, false )
	
	spaceFeedsManager:startSendingUpdates()
	
	canFetchUpdatesStream:expectDataWithinTime( 3, false )
	canSendUpdatesStream:expectDataWithinTime( 3, false )
	
	spaceFeedsManager:stopFetchingUpdates()
	
	canFetchUpdatesStream:expectDataWithinTime( 3, false )
	canSendUpdatesStream:expectDataWithinTime( 3, false )
	
	spaceFeedsManager:stopSendingUpdates()
	
	canFetchUpdatesStream:expectDataWithinTime( 3, false )
	canSendUpdatesStream:expectDataWithinTime( 3, false )
	
	-- Now that the catalog is actively syncing, we shouldn't be fetching/sending
	-- any updates.
	
	spaceFeedsManager:reportCatalogInitialSyncIsComplete( true )
	
	canFetchUpdatesStream:expectDataWithinTime( 3, false )
	canSendUpdatesStream:expectDataWithinTime( 3, false )
	
	spaceFeedsManager:startFetchingUpdates()
	
	canFetchUpdatesStream:expectDataWithinTime( 3, true )
	canSendUpdatesStream:expectDataWithinTime( 3, false )
	
	spaceFeedsManager:startSendingUpdates()
	
	canFetchUpdatesStream:expectDataWithinTime( 3, true )
	canSendUpdatesStream:expectDataWithinTime( 3, true )
	
	spaceFeedsManager:stopSendingUpdates()
	
	canFetchUpdatesStream:expectDataWithinTime( 3, true )
	canSendUpdatesStream:expectDataWithinTime( 3, false )

	spaceFeedsManager:stopFetchingUpdates()
	
	canFetchUpdatesStream:expectDataWithinTime( 3, false )
	canSendUpdatesStream:expectDataWithinTime( 3, false )


	canFetchUpdatesStream:dispose()
	canSendUpdatesStream:dispose()

end

--------------------------------------------------------------------------------

function WFCatSpaceFeedsManagerTest.rxTestIsDownloadingMetadata( rxTest )
	
	rxTest:setTimeLimit( 10 )
	
	local expectedResponses = { 
		{
	
			url = string.format( "/v2/spaces/%s/updates", spaceId1 ),

			-- somewhat minimized version of payload

			response = string.format( [[{
				"base": "https://photos.adobe.io",
				"updates": [
					{
						"id": "8462f5ba3b39fd050d3cd6b7352ba561",
						"created": "2015-05-27T00:34:56.669987Z",
						"updated": "2015-05-27T00:38:29.160369Z",
						"type": "asset",
						"subtype": "image",
						"revision_ids": [
							"a1e880f718a348b89663cf38f9f29d23",
							"cd1ab30cf8464ef2a9e736b50c0ba33d",
							"96f33a7215e04c9aa49346bbf8aae921",
							"813fdbda1b244c2d9f41008ba63eedba"
						],
						"payload": {
							"importSource": {
								"importTimestamp": "2015-05-26T17:34:23-07:00",
								"originalHeight": 1600,
								"originalWidth": 1068
							},
							"captureDate": "2015-05-17T15:50:12",
							"xmp": {
								"dc": {
									"rights": "test copyright string"
								}
							},
							"location": {
								"longitude": -93.600107,
								"latitude": 45.951733,
								"altitude": 377.9
							}
						}
					}
				],
				"links": {
					"next": {
						"href": "/v2/spaces/%s/updates?updated_since=pull1"
					}
				}
			}]], spaceId1 ),

			headers = {
				status_code = '200',
			},

			options = {
				bypassRateLimiter = true,
			}

		},

		{
			url = string.format( "/v2/spaces/%s/updates?updated_since=pull1", spaceId1 ),

			response = string.format( [[{
				"base": "https://photos.adobe.io",
				"updates": [
					{
						"id": "8462f5ba3b39fd050d3cd6b7352ba561",
						"created": "2015-05-27T00:34:56.669987Z",
						"updated": "2015-05-27T00:38:29.160369Z",
						"type": "asset",
						"subtype": "image",
						"revision_ids": [
							"a1e880f718a348b89663cf38f9f29d23",
							"cd1ab30cf8464ef2a9e736b50c0ba33d",
							"96f33a7215e04c9aa49346bbf8aae921",
							"813fdbda1b244c2d9f41008ba63eedba",
							"3a4a43dc2a9a4f1897c27f7a26e2e068"
						],
						"payload": {
							"xmp": {
								"dc": {
									"rights": "something changed"
								}
							},
							"importSource": {
								"importTimestamp": "2015-05-26T17:34:23-07:00",
								"originalHeight": 1600,
								"originalWidth": 1068
							},
							"captureDate": "2015-05-17T15:50:12"
						}
					}
				],
				"links": {
					"next": {
						"href": "/v2/spaces/%s/updates?updated_since=pull2"
					}
				}
			}]], spaceId1 ),

			headers = {
				status_code = '200',
			},

			options = {
				bypassRateLimiter = true,
			}

		},
		{
			url = string.format( "/v2/spaces/%s/updates?updated_since=pull2", spaceId1 ),

			response = string.format( [[{
				"base": "https://photos.adobe.io",
				"updates": [],
				"links": {
					"next": {
						"href": "/v2/spaces/%s/updates?updated_since=pull2"
					}
				}
			}]], spaceId1 ),

			headers = {
				status_code = '200',
			},

			options = {
				bypassRateLimiter = true,
			}

		},
		{
			url = string.format( "/v2/spaces/%s/updates", spaceId2 ),
			
			response = string.format( [[{
					"base": "https://photos.adobe.io",
					"updates": [],
					"links": {
						"next": {
							"href": "/v2/spaces/%s/updates?updated_since=pull1"
						}
					}
				}]], spaceId2 ),

			headers = {
				status_code = '200',
			},

			options = {
				bypassRateLimiter = true,
			}

		},
		{
			url = string.format( "/v2/spaces/%s/updates?updated_since=pull1", spaceId2 ),
			
			response = string.format( [[{
					"base": "https://photos.adobe.io",
					"updates": [],
					"links": {
						"next": {
							"href": "/v2/spaces/%s/updates?updated_since=pull1"
						}
					}
				}]], spaceId2 ),

			headers = {
				status_code = '200',
			},

			options = {
				bypassRateLimiter = true,
			}

		},
		{
			url = "/v2/spaces/updates?updated_since=pull1",

			response = string.format( [[{
				"base": "https://photos.adobe.io",
				"updates": [],
				"links": {
					"next": {
						"href": "/v2/spaces/updates?updated_since=pull1"
					}
				}
			}]], spaceId1 ),

			headers = {
				status_code = '200',
			},

			options = {
				bypassRateLimiter = true,
			}

		}
 
 	}

	local args = {
		spacesAreOverQuota = false,
		entitlementExpired = false,
	}
	
	local catalogSpec = createCatalogSpecForTest( rxTest, space1, space2, expectedResponses, args )
	
	local wfCatalog = createWFCatalogForTestWithSpec( rxTest, catalogSpec )
	
	local spaceFeedsManagerArgs = {
		taskDelays = {
			progressiveBackoff = false,
			noWorkDelay = 0.5,
			maxNoWorkDelay = 1,
			errorDelay = 1,
			maxDelay = 1,
		},
		isPrefetchEnabled = true,
		albumAssetsForFilteredAssetsQueryFn = albumAssetsForFilteredAssetsQueryFn,
		testMode = true,
	}

	local spaceFeedsManager = WFCatSpaceFeedsManager( wfCatalog, spaceFeedsManagerArgs )
	rxTest:addCloseTarget( spaceFeedsManager )
	
	local observer = rxTest:observe( spaceFeedsManager:isDownloadingMetadata() )
	
	observer:expectDataWithinTime( 2, false )
	
	-- Expect updates for space 1 & 2
	
	rxTest:expectOnlyCompleted( spaceFeedsManager:_testOnly_getUpdatesOnce() )
	
	observer:expectDataWithinTime( 2, true ) -- Space 1
	
	observer:expectDataWithinTime( 2, false )
	
	observer:expectDataWithinTime( 2, true ) -- Space 2
	
	observer:expectDataWithinTime( 2, false )
	
	-- Expect no updates.
	
	rxTest:expectOnlyCompleted( spaceFeedsManager:_testOnly_getUpdatesOnce() )
	
	observer:expectDataWithinTime( 2, false )
	
end


--------------------------------------------------------------------------------

function WFCatSpaceFeedsManagerTest.rxTestLapsedOwnerSubscription( rxTest )

	rxTest:setTimeLimit( 10 )
	
	local expectedResponses = {
		{
		
			url = "/v2/spaces/updates?updated_since=pull1",
			
			response = [[{
			  "base": "https://photos-stage.adobe.io/v2/",
			  "updates": [
				{
				  "created": "2019-08-21T17:01:02.467393Z",
				  "type": "space",
				  "subtype": "shared",
				  "id": "<space1_spaceId>",
				  "owner": {
					"id": "<space1_spaceOwnerId>",
					"outOfQuota": false,
					"entitlementExpired": false,
					"links": {
					  "self": {
						"href": "spaces/<space1_spaceId>/updates/<space1_spaceOwnerId>"
					  }
					}
				  },
				  "this_space_user": {
					"id": "<space1_youSpaceUserId>",
					"user": {
					  "id": "<space1_youFeedbackUserId>",
					  "links": {
						"self": {
						  "href": "spaces/<space1_spaceId>/updates/<space1_youFeedbackUserId>"
						}
					  }
					},
					"links": {
					  "self": {
						"href": "spaces/<space1_spaceId>/updates/<space1_youSpaceUserId>"
					  }
					}
				  },
				  "albums": [
					{
					  "id": "<album1_id>",
					  "links": {
						"self": {
						  "href": "spaces/<space1_spaceId>/updates/<album1_id>"
						}
					  },
					  "space_album": {
						"id": "<spaceAlbum1_id>",
						"links": {
						  "self": {
							"href": "spaces/<space1_spaceId>/updates/<spaceAlbum1_id>"
						  }
						}
					  }
					}
				  ],
				  "revision_ids": [
					"33ca44a7da51f56e6d3f9e80b9181aaf",
					"813254e958ea3ffac99d32cda7acc969",
					"b6088deb146acbbde9268c50156fa19a",
					"a0cb906593fa65713d198cec5407426a",
					"3a7c6f13acb6fd7afa9d979681ae1e20",
					"8f8b861e17b67454062ab16b7437b189",
					"2ddfea16ac1a4af1cd0b9a1937fc9c3a",
					"d86b4baa17e22e23bf1a87e84e7d9807"
				  ],
				  "updated": "2019-08-21T17:01:08.471614Z",
				  "country_blocked": false,
				  "payload": {
					"userUpdated": "2019-08-21T17:01:08.445000Z",
					"allowInviteRequests": true,
					"userCreated": "2019-08-21T17:01:02.439000Z",
					"presentationFilters": true,
					"location": true,
					"download": true,
					"private": true,
					"metadata": true
				  },
				  "createdOnClient": "Chrome-76.0.3809-MacOSX",
				  "links": {
					"self": {
					  "href": "spaces/<space1_spaceId>/updates/<space1_spaceId>"
					},
					"current": {
					  "href": "spaces/<space1_spaceId>/updates?updated_since=pull1"
					},
					"/rels/web_space": {
					  "href": "http://bit.ly/2ZkCLDo"
					},
					"start": {
					  "href": "spaces/<space1_spaceId>/updates"
					}
				  }
				}
			  ],
			  "links": {
				"next": {
				  "href": "/v2/spaces/updates?updated_since=pull2"
				}
			  }
			}]],
			
			responseFilter = function( response )

				response = response:gsub( '<space1_spaceId>', space1.id )
				response = response:gsub( '<space1_youSpaceUserId>', space1.youSpaceUserId )
				response = response:gsub( '<space1_spaceOwnerId>', space1.ownerId or KSIdGenerator.uuidInLowercaseHex() )
				response = response:gsub( '<space1_youFeedbackUserId>', space1.youFeedbackUserId or KSIdGenerator.uuidInLowercaseHex() ) 
				
				response = response:gsub( '<album1_id>', space1.albumId )
				response = response:gsub( '<spaceAlbum1_id>', space1.spaceAlbumId )

				return response

			end,
			
			headers = {
				status_code = '200',
			},
			
			options = {
				bypassRateLimiter = true,
			}
				
		},
		
		{
			url = string.format( "/v2/spaces/%s/updates", spaceId1 ),

			response = string.format( [[{
				"base": "https://photos.adobe.io",
				"updates": [ {
						"id": "8462f5ba3b39fd050d3cd6b7352ba561",
						"created": "2015-05-27T00:34:56.669987Z",
						"updated": "2015-05-27T00:38:29.160369Z",
						"type": "asset",
						"subtype": "image",
						"revision_ids": [
							"a1e880f718a348b89663cf38f9f29d23",
							"cd1ab30cf8464ef2a9e736b50c0ba33d",
							"96f33a7215e04c9aa49346bbf8aae921",
							"813fdbda1b244c2d9f41008ba63eedba"
						],
						"payload": {
							"importSource": {
								"importTimestamp": "2015-05-26T17:34:23-07:00",
								"originalHeight": 1600,
								"originalWidth": 1068
							},
							"captureDate": "2015-05-17T15:50:12",
							"xmp": {
								"dc": {
									"rights": "test copyright string"
								}
							},
							"location": {
								"longitude": -93.600107,
								"latitude": 45.951733,
								"altitude": 377.9
							}
						}
					}
				],
				"links": {
					"next": {
						"href": "/v2/spaces/%s/updates?updated_since=pull1"
					}
				}
			}]], spaceId1 ),

			headers = {
				status_code = '200',
			},

			options = {
				bypassRateLimiter = true,
			}

		},
		{
			url = string.format( "/v2/spaces/%s/updates?updated_since=pull1", spaceId1 ),

			response = string.format( [[{
				"base": "https://photos.adobe.io",
				"updates": [],
				"links": {
					"next": {
						"href": "/v2/spaces/%s/updates?updated_since=pull1"
					}
				}
			}]], spaceId1 ),

			headers = {
				status_code = '200',
			},

			options = {
				bypassRateLimiter = true,
			}

		}
		
 	}
	
	local args = {
		spacesAreOverQuota = false,
		entitlementExpired = true,
	}
	
	local catalogSpec = createCatalogSpecForTest( rxTest, space1, space2, expectedResponses, args )
	
	local wfCatalog = createWFCatalogForTestWithSpec( rxTest, catalogSpec )
	
	local spaceFeedsManagerArgs = {
		taskDelays = {
			progressiveBackoff = false,
			noWorkDelay = 0.5,
			maxNoWorkDelay = 1,
			errorDelay = 1,
			maxDelay = 1,
		},
		isPrefetchEnabled = true,
		albumAssetsForFilteredAssetsQueryFn = albumAssetsForFilteredAssetsQueryFn,
		testMode = true,
	}

	local spaceFeedsManager = WFCatSpaceFeedsManager( wfCatalog, spaceFeedsManagerArgs )
	rxTest:addCloseTarget( spaceFeedsManager )
	
	rxTest:expectOnlyCompleted( spaceFeedsManager:_testOnly_getUpdatesOnce() )
	
	-- Make sure that we now have the required local documents.
	
	local observer = rxTest:observe( wfCatalog:indexStore():query( {
			sql = [[ SELECT COUNT( * ) FROM feedback_user ]],
		} ):map( function( rows ) return rows[ 1 ][ 1 ] end ) )
			-- This checks for the feedback user docs
			
	observer:expectDataWithinTime( 2, 4 )
	
	-- Check for Albums / Space Albums
	
	rxTest:expectSingleValue( wfCatalog:docStore():docValue( albumId1 )
		:map( function( docValue )
			return not not docValue._empty
		end ), false )
		
	rxTest:expectSingleValue( wfCatalog:docStore():docValue( spaceAlbumId1 )
		:map( function( docValue )
			return not not docValue._empty
		end ), false )
	
	rxTest:expectSingleValue( wfCatalog:docStore():docValue( albumId2 )
		:map( function( docValue )
			return not not docValue._empty
		end ), false )
		
	rxTest:expectSingleValue( wfCatalog:docStore():docValue( spaceAlbumId2 )
		:map( function( docValue )
			return not not docValue._empty
		end ), false )
		
	-- Verify the space pusher is aware of of the lapsed subscription.
	
	rxTest:expectEqual( true, spaceFeedsManager._spacePusher:getSpaceIsExpired( spaceId1 ) )
		
	-- Add an asset to the space.

	local assetDocId = '44ab164cd874418ca566d5a0e259ac0d'

	local assetDoc = buildDocValue( rxTest, wfCatalog, assetDocId )
	assetDoc._spaceId = spaceId1
	
	rxTest:expectSingleValue( wfCatalog:docStore():setDocValue( assetDoc ), ':ignore:' )
	
	-- Trigger the space metadata pusher.
	
	local sendNUpdates = rxTest:observe( spaceFeedsManager._spacePusher:_DEBUG_sendUpdatesOnce() )
	sendNUpdates:expectCompletedWithinTime( 5 )
	sendNUpdates:dispose()
	
	-- Verify the asset doc is marked as over quota.
	
	rxTest:expectSingleValue(

		wfCatalog:docStore():docValue( assetDocId )
			:map( function( docValue )
				return KSTableUtils.derefOpt( docValue, '_localOnly', 'ozError', 'subtype' )
			end ), 'TrialOrSubscriptionExpiredError' )

	-- Get more updates. This should update spaceId1 as not expired.
	
	local activeSpacesObserver = rxTest:observe( spaceFeedsManager._activeSpacesStream )
	
	rxTest:expectOnlyCompleted( spaceFeedsManager:_testOnly_getUpdatesOnce() )
	
	activeSpacesObserver:expectDataWithinTime( 2, spaceId1, spaceUserId1, ":ignore:", false, ":ignore:", ":ignore:" )
	
	activeSpacesObserver:dispose()
	
	-- Verify the asset doc is no longer marked as over quota.
	
	local errorObserver = rxTest:observe( wfCatalog:docStore():ongoingDocValue( assetDocId )
			:map( function( docValue )
				return KSTableUtils.derefOpt( docValue, '_localOnly', 'ozError', 'subtype' )
			end ) )
			
	errorObserver:expectDataWithinTime( 5, nil )
	
	errorObserver:dispose()
	
end

--------------------------------------------------------------------------------

function WFCatSpaceFeedsManagerTest.rxTestInsufficientPermission_ViewerToContributor( rxTest )

	rxTest:setTimeLimit( 30 )

	local assetDocId = '44ab164cd874418ca566d5a0e259ac0d'
	local newRevId1 =  'rrrr164cd874418ca566d5a0e259ac0d'
	local captureDate = '2019-09-26T18:13:30.050Z'
	
	local expectedResponses = {
		{
			url = string.format( "/v2/spaces/%s/updates", spaceId1 ),

			response = string.format( [[{
				"base": "https://photos.adobe.io",
				"updates": [ {
						"id": "8462f5ba3b39fd050d3cd6b7352ba561",
						"created": "2015-05-27T00:34:56.669987Z",
						"updated": "2015-05-27T00:38:29.160369Z",
						"type": "asset",
						"subtype": "image",
						"revision_ids": [
							"a1e880f718a348b89663cf38f9f29d23",
							"cd1ab30cf8464ef2a9e736b50c0ba33d",
							"96f33a7215e04c9aa49346bbf8aae921",
							"813fdbda1b244c2d9f41008ba63eedba"
						],
						"payload": {
							"importSource": {
								"importTimestamp": "2015-05-26T17:34:23-07:00",
								"originalHeight": 1600,
								"originalWidth": 1068
							},
							"captureDate": "2015-05-17T15:50:12",
							"xmp": {
								"dc": {
									"rights": "test copyright string"
								}
							},
							"location": {
								"longitude": -93.600107,
								"latitude": 45.951733,
								"altitude": 377.9
							}
						}
					}
				],
				"links": {
					"next": {
						"href": "/v2/spaces/%s/updates?updated_since=pull1"
					}
				}
			}]], spaceId1 ),

			headers = {
				status_code = '200',
			},

			options = {
				bypassRateLimiter = true,
			}

		},
		{
			url = string.format( "/v2/spaces/%s/updates?updated_since=pull1", spaceId1 ),

			response = string.format( [[{
				"base": "https://photos.adobe.io",
				"updates": [],
				"links": {
					"next": {
						"href": "/v2/spaces/%s/updates?updated_since=pull1"
					}
				}
			}]], spaceId1 ),

			headers = {
				status_code = '200',
			},

			options = {
				bypassRateLimiter = true,
			}

		},
		{
			url = string.format( "/v2/spaces/%s/updates", spaceId2 ),

			response = string.format( [[{
				"base": "https://photos.adobe.io",
				"updates": [ {
						"id": "2222f5ba3b39fd050d3cd6b7352ba561",
						"created": "2015-05-27T00:34:56.669987Z",
						"updated": "2015-05-27T00:38:29.160369Z",
						"type": "asset",
						"subtype": "image",
						"revision_ids": [
							"22e880f718a348b89663cf38f9f29d23",
							"331ab30cf8464ef2a9e736b50c0ba33d"
						],
						"payload": {
							"importSource": {
								"importTimestamp": "2015-05-26T17:34:23-07:00",
								"originalHeight": 1600,
								"originalWidth": 1068
							},
							"captureDate": "2015-05-17T15:50:12",
							"xmp": {
								"dc": {
									"rights": "test copyright string"
								}
							},
							"location": {
								"longitude": -93.600107,
								"latitude": 45.951733,
								"altitude": 377.9
							}
						}
					}
				],
				"links": {
					"next": {
						"href": "/v2/spaces/%s/updates?updated_since=pull1"
					}
				}
			}]], spaceId2 ),

			headers = {
				status_code = '200',
			},

			options = {
				bypassRateLimiter = true,
			}

		},
		{
			url = string.format( "/v2/spaces/%s/updates?updated_since=pull1", spaceId2 ),

			response = string.format( [[{
				"base": "https://photos.adobe.io",
				"updates": [],
				"links": {
					"next": {
						"href": "/v2/spaces/%s/updates?updated_since=pull1"
					}
				}
			}]], spaceId2 ),

			headers = {
				status_code = '200',
			},

			options = {
				bypassRateLimiter = true,
			}

		},
		{
			url = string.format( "/v2/spaces/updates" ),
				
			data = string.format( [[[{"space_id":"%s","type":"asset","action":"create","revision_id":"%s","id":"%s","payload":{"importSource":{},"captureDate":"%s"},"subtype":"image"}] ]], 
					spaceId1, newRevId1, assetDocId, captureDate ),
			
			dataFilter = function( str )
			
				str = str:gsub( '"sha256":"[0-9a-f]+"', "" ) .. ' '

				return str
				
			end,
		
			options = {
				requestHeaders = {
					{
						header = "Content-Type",
						value = "application/json",
					},
					{
						header = "Accept",
						value = "application/json",
					},
				},
				requestMethod = "POST",
				bypassRateLimiter = true,
			},

		},
		{
			url = "/v2/spaces/updates?updated_since=pull1",
			
			response = [[{
			  "base": "https://photos-stage.adobe.io/v2/",
			  "updates": [
				{
				  "created": "2019-08-21T17:01:02.467393Z",
				  "type": "space",
				  "subtype": "shared",
				  "id": "<space1_spaceId>",
				  "owner": {
					"id": "<space1_spaceOwnerId>",
					"outOfQuota": false,
					"entitlementExpired": false,
					"links": {
					  "self": {
						"href": "spaces/<space1_spaceId>/updates/<space1_spaceOwnerId>"
					  }
					}
				  },
				  "this_space_user": {
					"id": "<space1_youSpaceUserId>",
					"user": {
					  "id": "<space1_youFeedbackUserId>",
					  "links": {
						"self": {
						  "href": "spaces/<space1_spaceId>/updates/<space1_youFeedbackUserId>"
						}
					  }
					},
					"links": {
					  "self": {
						"href": "spaces/<space1_spaceId>/updates/<space1_youSpaceUserId>"
					  }
					}
				  },
				  "albums": [
					{
					  "id": "<album1_id>",
					  "links": {
						"self": {
						  "href": "spaces/<space1_spaceId>/updates/<album1_id>"
						}
					  },
					  "space_album": {
						"id": "<spaceAlbum1_id>",
						"links": {
						  "self": {
							"href": "spaces/<space1_spaceId>/updates/<spaceAlbum1_id>"
						  }
						}
					  }
					}
				  ],
				  "revision_ids": [
					"33ca44a7da51f56e6d3f9e80b9181aaf",
					"813254e958ea3ffac99d32cda7acc969",
					"b6088deb146acbbde9268c50156fa19a",
					"a0cb906593fa65713d198cec5407426a",
					"3a7c6f13acb6fd7afa9d979681ae1e20",
					"8f8b861e17b67454062ab16b7437b189",
					"2ddfea16ac1a4af1cd0b9a1937fc9c3a",
					"d86b4baa17e22e23bf1a87e84e7d9807",
					"356b4baa17e22e23bf1a87e84e7d9807"
				  ],
				  "updated": "2019-08-22T17:01:08.471614Z",
				  "country_blocked": false,
				  "payload": {
					"userUpdated": "2019-08-21T17:01:08.445000Z",
					"allowInviteRequests": true,
					"userCreated": "2019-08-21T17:01:02.439000Z",
					"presentationFilters": true,
					"location": true,
					"download": true,
					"private": true,
					"metadata": true
				  },
				  "createdOnClient": "Chrome-76.0.3809-MacOSX",
				  "links": {
					"self": {
					  "href": "spaces/<space1_spaceId>/updates/<space1_spaceId>"
					},
					"current": {
					  "href": "spaces/<space1_spaceId>/updates?updated_since=pull2"
					},
					"/rels/web_space": {
					  "href": "http://bit.ly/2ZkCLDo"
					},
					"start": {
					  "href": "spaces/<space1_spaceId>/updates"
					}
				  }
				}
			  ],
			  "links": {
				"next": {
				  "href": "/v2/spaces/updates?updated_since=pull2"
				}
			  }
			}]],
			
			responseFilter = function( response )

				response = response:gsub( '<space1_spaceId>', space1.id )
				response = response:gsub( '<space1_youSpaceUserId>', space1.youSpaceUserId )
				response = response:gsub( '<space1_spaceOwnerId>', space1.ownerId or KSIdGenerator.uuidInLowercaseHex() )
				response = response:gsub( '<space1_youFeedbackUserId>', space1.youFeedbackUserId or KSIdGenerator.uuidInLowercaseHex() ) 
				
				response = response:gsub( '<album1_id>', space1.albumId )
				response = response:gsub( '<spaceAlbum1_id>', space1.spaceAlbumId )

				return response

			end,
			
			headers = {
				status_code = '200',
			},
			
			options = {
				bypassRateLimiter = true,
			}
				
		},
		{
			url = string.format( "/v2/spaces/%s/updates?updated_since=pull1", spaceId1 ),
			
			response = string.format( [[{
				"base": "https://photos.adobe.io",
				"updates": [ {
					  "updated": "2019-09-26T21:20:54.529292Z",
					  "type": "error",
					  "source": {
						"id": "%s",
						"type": "asset",
						"action": "create",
						"revision_id": "%s"
					  },
					  "subtype": "InsufficientPermissionError",
					  "id": "08eab133e5ba2161393aed62bee916d7",
					  "errors": {},
					  "status": 403
					}
				],
				"links": {
					"next": {
						"href": "/v2/spaces/%s/updates?updated_since=pull2"
					}
				}
			}]], assetDocId, newRevId1, spaceId1 ),

			responseFilter = function( response )

				response = response:gsub( '<space1_spaceId>', space1.id )
				response = response:gsub( '<space1_youSpaceUserId>', space1.youSpaceUserId )

				return response

			end,

			headers = {
				status_code = '200',
			},
			
			options = {
				bypassRateLimiter = true,
			}
			
		},
		{
			url = string.format( "/v2/spaces/%s/updates?updated_since=pull2", spaceId1 ),

			response = string.format( [[{
				"base": "https://photos.adobe.io",
				"updates": [],
				"links": {
					"next": {
						"href": "/v2/spaces/%s/updates?updated_since=pull2"
					}
				}
			}]], spaceId1 ),

			headers = {
				status_code = '200',
			},

			options = {
				bypassRateLimiter = true,
			}

		},
		{
			url = "/v2/spaces/updates?updated_since=pull2",
			
			response = [[{
			  "base": "https://photos-stage.adobe.io/v2/",
			  "updates": [
				{
				  "created": "2019-08-21T17:01:02.467393Z",
				  "type": "space",
				  "subtype": "shared",
				  "id": "<space1_spaceId>",
				  "owner": {
					"id": "<space1_spaceOwnerId>",
					"outOfQuota": false,
					"entitlementExpired": false,
					"links": {
					  "self": {
						"href": "spaces/<space1_spaceId>/updates/<space1_spaceOwnerId>"
					  }
					}
				  },
				  "this_space_user": {
					"id": "<space1_youSpaceUserId>",
					"user": {
					  "id": "<space1_youFeedbackUserId>",
					  "links": {
						"self": {
						  "href": "spaces/<space1_spaceId>/updates/<space1_youFeedbackUserId>"
						}
					  }
					},
					"links": {
					  "self": {
						"href": "spaces/<space1_spaceId>/updates/<space1_youSpaceUserId>"
					  }
					}
				  },
				  "albums": [
					{
					  "id": "<album1_id>",
					  "links": {
						"self": {
						  "href": "spaces/<space1_spaceId>/updates/<album1_id>"
						}
					  },
					  "space_album": {
						"id": "<spaceAlbum1_id>",
						"links": {
						  "self": {
							"href": "spaces/<space1_spaceId>/updates/<spaceAlbum1_id>"
						  }
						}
					  }
					}
				  ],
				  "revision_ids": [
					"33ca44a7da51f56e6d3f9e80b9181aaf",
					"813254e958ea3ffac99d32cda7acc969",
					"b6088deb146acbbde9268c50156fa19a",
					"a0cb906593fa65713d198cec5407426a",
					"3a7c6f13acb6fd7afa9d979681ae1e20",
					"8f8b861e17b67454062ab16b7437b189",
					"2ddfea16ac1a4af1cd0b9a1937fc9c3a",
					"d86b4baa17e22e23bf1a87e84e7d9807"
				  ],
				  "updated": "2019-08-21T17:01:08.471614Z",
				  "country_blocked": false,
				  "payload": {
					"userUpdated": "2019-08-21T17:01:08.445000Z",
					"allowInviteRequests": true,
					"userCreated": "2019-08-21T17:01:02.439000Z",
					"presentationFilters": true,
					"location": true,
					"download": true,
					"private": true,
					"metadata": true
				  },
				  "createdOnClient": "Chrome-76.0.3809-MacOSX",
				  "links": {
					"self": {
					  "href": "spaces/<space1_spaceId>/updates/<space1_spaceId>"
					},
					"current": {
					  "href": "spaces/<space1_spaceId>/updates?updated_since=pull3"
					},
					"/rels/web_space": {
					  "href": "http://bit.ly/2ZkCLDo"
					},
					"start": {
					  "href": "spaces/<space1_spaceId>/updates"
					}
				  }
				}
			  ],
			  "links": {
				"next": {
				  "href": "/v2/spaces/updates?updated_since=pull2"
				}
			  }
			}]],
			
			responseFilter = function( response )

				response = response:gsub( '<space1_spaceId>', space1.id )
				response = response:gsub( '<space1_youSpaceUserId>', space1.youSpaceUserId )
				response = response:gsub( '<space1_spaceOwnerId>', space1.ownerId or KSIdGenerator.uuidInLowercaseHex() )
				response = response:gsub( '<space1_youFeedbackUserId>', space1.youFeedbackUserId or KSIdGenerator.uuidInLowercaseHex() ) 
				
				response = response:gsub( '<album1_id>', space1.albumId )
				response = response:gsub( '<spaceAlbum1_id>', space1.spaceAlbumId )

				return response

			end,
			
			headers = {
				status_code = '200',
			},
			
			options = {
				bypassRateLimiter = true,
			}
				
		},
		{
			url = string.format( "/v2/spaces/%s/updates?updated_since=pull2", spaceId1 ),
			
			response = string.format( [[{
				"base": "https://photos.adobe.io",
				"updates": [ {
					"updated": "2019-07-25T22:27:41.466149Z",
					  "type": "space_user",
					  "payload": {
						"status": "accepted",
						"role": "contributor",
						"userUpdated": "2019-07-25T22:27:41.299863Z",
						"userCreated": "2019-07-25T22:27:41.299863Z"
					  },
					  "id": "<space1_youSpaceUserId>",
					  "user": {
						"id": "87bd59dd4c01e4f71155d7bfb50ccdf3"
					  },
					  "created": "2019-07-25T22:27:41.466149Z",
					  "revision_ids": [
						"ae43e159e80148fb47a8fe3ec4e6a718"
					  ]
					}
				],
				"links": {
					"next": {
						"href": "/v2/spaces/%s/updates?updated_since=pull3"
					}
				}
			}]], spaceId1 ),
			
			responseFilter = function( response )

				response = response:gsub( '<space1_spaceId>', space1.id )
				response = response:gsub( '<space1_youSpaceUserId>', space1.youSpaceUserId )

				return response

			end,

			headers = {
				status_code = '200',
			},
			
			options = {
				bypassRateLimiter = true,
			}
			
		},
		
		{
			url = string.format( "/v2/spaces/%s/updates?updated_since=pull3", spaceId1 ),

			response = string.format( [[{
				"base": "https://photos.adobe.io",
				"updates": [],
				"links": {
					"next": {
						"href": "/v2/spaces/%s/updates?updated_since=pull3"
					}
				}
			}]], spaceId1 ),

			headers = {
				status_code = '200',
			},

			options = {
				bypassRateLimiter = true,
			}

		},

	}
	
	local args = {
		spaceUser1Role = 'viewer'
	}
	
	local catalogSpec = createCatalogSpecForTest( rxTest, space1, space2, expectedResponses, args )

	local wfCatalog = createWFCatalogForTestWithSpec( rxTest, catalogSpec )
	
	local errorHandler = WFCatErrorHandler( wfCatalog,{
		deleteAssets = bulkDelete,
		deleteIndividualAlbum = deleteDoc },
		{ debugMode = true } )
	
	rxTest:addCloseTarget( errorHandler )
	
	local spaceFeedsManagerArgs = {
		taskDelays = {
			progressiveBackoff = false,
			noWorkDelay = 0.5,
			maxNoWorkDelay = 1,
			errorDelay = 1,
			maxDelay = 1,
		},
		albumAssetsForFilteredAssetsQueryFn = albumAssetsForFilteredAssetsQueryFn,
		testMode = true
	}

	-- Add an asset to the space.

	local assetDoc = buildDocValue( rxTest, wfCatalog, assetDocId )
	assetDoc._spaceId = spaceId1
	assetDoc._newRevId = newRevId1
	assetDoc.develop = nil
	KSTableUtils.setNested( assetDoc, captureDate, 'captureDate' )
	
	rxTest:expectSingleValue( wfCatalog:docStore():setDocValue( assetDoc ), ':ignore:' )

	-- Start up the space feed manager

	local spaceFeedsManager = WFCatSpaceFeedsManager( wfCatalog, spaceFeedsManagerArgs )
	rxTest:addCloseTarget( spaceFeedsManager )
	
	-- Verify the active spaces.
	
	local activeSpacesObserver = rxTest:observe( spaceFeedsManager._activeSpacesStream )
	
	local isDownloadingObserver = rxTest:observe( spaceFeedsManager:isDownloadingMetadata() )
	
	rxTest:expectOnlyCompleted( spaceFeedsManager:_testOnly_getUpdatesOnce() )
	
	activeSpacesObserver:expectDataWithinTime( 2, spaceId1, spaceUserId1, ":ignore:", false, ":ignore:", ":ignore:" )
	activeSpacesObserver:expectDataWithinTime( 2, spaceId2, spaceUserId2, ":ignore:", false, ":ignore:", ":ignore:" )
	
	isDownloadingObserver:expectDataWithinTime( 5, true ) -- Space 1
	isDownloadingObserver:expectDataWithinTime( 5, false )
	
	isDownloadingObserver:expectDataWithinTime( 5, true ) -- Space 2
	isDownloadingObserver:expectDataWithinTime( 5, false )
	
	isDownloadingObserver:dispose()
	
	-- Trigger the space metadata pusher.
	
	local sendNUpdates = rxTest:observe( spaceFeedsManager._spacePusher:_DEBUG_sendUpdatesOnce() )
	sendNUpdates:expectCompletedWithinTime( 5 )
	sendNUpdates:dispose()
	
	-- Get more updates. This should fetch the error doc created from attempting
	-- to push the update.
	
	rxTest:expectOnlyCompleted( spaceFeedsManager:_testOnly_getUpdatesOnce() )
	
	-- Verify the asset doc is marked with the insufficient permission error.
	
	local errorObserver = rxTest:observe( wfCatalog:docStore():ongoingDocValue( assetDocId )
			:map( function( docValue )
				return KSTableUtils.derefOpt( docValue, '_localOnly', 'ozError', 'subtype' )
			end ) )
			
	errorObserver:expectDataWithinTime( 5, 'InsufficientPermissionError' )

	-- Get more updates. This should update spaceUser1 to a contributor
	
	rxTest:expectOnlyCompleted( spaceFeedsManager:_testOnly_getUpdatesOnce() )
	
	activeSpacesObserver:expectDataWithinTime( 2, spaceId1, spaceUserId1, ":ignore:", false, ":ignore:", ":ignore:" )
	
	activeSpacesObserver:dispose()
	
	-- Verify the asset doc is no longer marked as having an error.
			
	errorObserver:expectDataWithinTime( 5, nil )
	
	errorObserver:dispose()
	
end

--------------------------------------------------------------------------------

function WFCatSpaceFeedsManagerTest.rxTestInsufficientPermission_ViewerToEditor( rxTest )

	rxTest:setTimeLimit( 30 )

	local assetDocId = '44ab164cd874418ca566d5a0e259ac0d'
	local newRevId1 =  'rrrr164cd874418ca566d5a0e259ac0d'
	local captureDate = '2019-09-26T18:13:30.050Z'
	
	local expectedResponses = {
		{
			url = string.format( "/v2/spaces/%s/updates", spaceId1 ),

			response = string.format( [[{
				"base": "https://photos.adobe.io",
				"updates": [ {
						"id": "8462f5ba3b39fd050d3cd6b7352ba561",
						"created": "2015-05-27T00:34:56.669987Z",
						"updated": "2015-05-27T00:38:29.160369Z",
						"type": "asset",
						"subtype": "image",
						"revision_ids": [
							"a1e880f718a348b89663cf38f9f29d23",
							"cd1ab30cf8464ef2a9e736b50c0ba33d",
							"96f33a7215e04c9aa49346bbf8aae921",
							"813fdbda1b244c2d9f41008ba63eedba"
						],
						"payload": {
							"importSource": {
								"importTimestamp": "2015-05-26T17:34:23-07:00",
								"originalHeight": 1600,
								"originalWidth": 1068
							},
							"captureDate": "2015-05-17T15:50:12",
							"xmp": {
								"dc": {
									"rights": "test copyright string"
								}
							},
							"location": {
								"longitude": -93.600107,
								"latitude": 45.951733,
								"altitude": 377.9
							}
						}
					}
				],
				"links": {
					"next": {
						"href": "/v2/spaces/%s/updates?updated_since=pull1"
					}
				}
			}]], spaceId1 ),

			headers = {
				status_code = '200',
			},

			options = {
				bypassRateLimiter = true,
			}

		},
		{
			url = string.format( "/v2/spaces/%s/updates?updated_since=pull1", spaceId1 ),

			response = string.format( [[{
				"base": "https://photos.adobe.io",
				"updates": [],
				"links": {
					"next": {
						"href": "/v2/spaces/%s/updates?updated_since=pull1"
					}
				}
			}]], spaceId1 ),

			headers = {
				status_code = '200',
			},

			options = {
				bypassRateLimiter = true,
			}

		},
		{
			url = string.format( "/v2/spaces/%s/updates", spaceId2 ),

			response = string.format( [[{
				"base": "https://photos.adobe.io",
				"updates": [ {
						"id": "2222f5ba3b39fd050d3cd6b7352ba561",
						"created": "2015-05-27T00:34:56.669987Z",
						"updated": "2015-05-27T00:38:29.160369Z",
						"type": "asset",
						"subtype": "image",
						"revision_ids": [
							"22e880f718a348b89663cf38f9f29d23",
							"331ab30cf8464ef2a9e736b50c0ba33d"
						],
						"payload": {
							"importSource": {
								"importTimestamp": "2015-05-26T17:34:23-07:00",
								"originalHeight": 1600,
								"originalWidth": 1068
							},
							"captureDate": "2015-05-17T15:50:12",
							"xmp": {
								"dc": {
									"rights": "test copyright string"
								}
							},
							"location": {
								"longitude": -93.600107,
								"latitude": 45.951733,
								"altitude": 377.9
							}
						}
					}
				],
				"links": {
					"next": {
						"href": "/v2/spaces/%s/updates?updated_since=pull1"
					}
				}
			}]], spaceId2 ),

			headers = {
				status_code = '200',
			},

			options = {
				bypassRateLimiter = true,
			}

		},
		{
			url = string.format( "/v2/spaces/%s/updates?updated_since=pull1", spaceId2 ),

			response = string.format( [[{
				"base": "https://photos.adobe.io",
				"updates": [],
				"links": {
					"next": {
						"href": "/v2/spaces/%s/updates?updated_since=pull1"
					}
				}
			}]], spaceId2 ),

			headers = {
				status_code = '200',
			},

			options = {
				bypassRateLimiter = true,
			}

		},
		{
			url = string.format( "/v2/spaces/updates" ),
				
			data = string.format( [[[{"space_id":"%s","type":"asset","action":"create","revision_id":"%s","id":"%s","payload":{"importSource":{},"captureDate":"%s"},"subtype":"image"}] ]], 
					spaceId1, newRevId1, assetDocId, captureDate ),
			
			dataFilter = function( str )
			
				str = str:gsub( '"sha256":"[0-9a-f]+"', "" ) .. ' '

				return str
				
			end,
		
			options = {
				requestHeaders = {
					{
						header = "Content-Type",
						value = "application/json",
					},
					{
						header = "Accept",
						value = "application/json",
					},
				},
				requestMethod = "POST",
				bypassRateLimiter = true,
			},

		},
		{
			url = "/v2/spaces/updates?updated_since=pull1",
			
			response = [[{
			  "base": "https://photos-stage.adobe.io/v2/",
			  "updates": [
				{
				  "created": "2019-08-21T17:01:02.467393Z",
				  "type": "space",
				  "subtype": "shared",
				  "id": "<space1_spaceId>",
				  "owner": {
					"id": "<space1_spaceOwnerId>",
					"outOfQuota": false,
					"entitlementExpired": false,
					"links": {
					  "self": {
						"href": "spaces/<space1_spaceId>/updates/<space1_spaceOwnerId>"
					  }
					}
				  },
				  "this_space_user": {
					"id": "<space1_youSpaceUserId>",
					"user": {
					  "id": "<space1_youFeedbackUserId>",
					  "links": {
						"self": {
						  "href": "spaces/<space1_spaceId>/updates/<space1_youFeedbackUserId>"
						}
					  }
					},
					"links": {
					  "self": {
						"href": "spaces/<space1_spaceId>/updates/<space1_youSpaceUserId>"
					  }
					}
				  },
				  "albums": [
					{
					  "id": "<album1_id>",
					  "links": {
						"self": {
						  "href": "spaces/<space1_spaceId>/updates/<album1_id>"
						}
					  },
					  "space_album": {
						"id": "<spaceAlbum1_id>",
						"links": {
						  "self": {
							"href": "spaces/<space1_spaceId>/updates/<spaceAlbum1_id>"
						  }
						}
					  }
					}
				  ],
				  "revision_ids": [
					"33ca44a7da51f56e6d3f9e80b9181aaf",
					"813254e958ea3ffac99d32cda7acc969",
					"b6088deb146acbbde9268c50156fa19a",
					"a0cb906593fa65713d198cec5407426a",
					"3a7c6f13acb6fd7afa9d979681ae1e20",
					"8f8b861e17b67454062ab16b7437b189",
					"2ddfea16ac1a4af1cd0b9a1937fc9c3a",
					"d86b4baa17e22e23bf1a87e84e7d9807",
					"356b4baa17e22e23bf1a87e84e7d9807"
				  ],
				  "updated": "2019-08-22T17:01:08.471614Z",
				  "country_blocked": false,
				  "payload": {
					"userUpdated": "2019-08-21T17:01:08.445000Z",
					"allowInviteRequests": true,
					"userCreated": "2019-08-21T17:01:02.439000Z",
					"presentationFilters": true,
					"location": true,
					"download": true,
					"private": true,
					"metadata": true
				  },
				  "createdOnClient": "Chrome-76.0.3809-MacOSX",
				  "links": {
					"self": {
					  "href": "spaces/<space1_spaceId>/updates/<space1_spaceId>"
					},
					"current": {
					  "href": "spaces/<space1_spaceId>/updates?updated_since=pull2"
					},
					"/rels/web_space": {
					  "href": "http://bit.ly/2ZkCLDo"
					},
					"start": {
					  "href": "spaces/<space1_spaceId>/updates"
					}
				  }
				}
			  ],
			  "links": {
				"next": {
				  "href": "/v2/spaces/updates?updated_since=pull2"
				}
			  }
			}]],
			
			responseFilter = function( response )

				response = response:gsub( '<space1_spaceId>', space1.id )
				response = response:gsub( '<space1_youSpaceUserId>', space1.youSpaceUserId )
				response = response:gsub( '<space1_spaceOwnerId>', space1.ownerId or KSIdGenerator.uuidInLowercaseHex() )
				response = response:gsub( '<space1_youFeedbackUserId>', space1.youFeedbackUserId or KSIdGenerator.uuidInLowercaseHex() ) 
				
				response = response:gsub( '<album1_id>', space1.albumId )
				response = response:gsub( '<spaceAlbum1_id>', space1.spaceAlbumId )

				return response

			end,
			
			headers = {
				status_code = '200',
			},
			
			options = {
				bypassRateLimiter = true,
			}
				
		},
		{
			url = string.format( "/v2/spaces/%s/updates?updated_since=pull1", spaceId1 ),
			
			response = string.format( [[{
				"base": "https://photos.adobe.io",
				"updates": [ {
					  "updated": "2019-09-26T21:20:54.529292Z",
					  "type": "error",
					  "source": {
						"id": "%s",
						"type": "asset",
						"action": "create",
						"revision_id": "%s"
					  },
					  "subtype": "InsufficientPermissionError",
					  "id": "08eab133e5ba2161393aed62bee916d7",
					  "errors": {},
					  "status": 403
					}
				],
				"links": {
					"next": {
						"href": "/v2/spaces/%s/updates?updated_since=pull2"
					}
				}
			}]], assetDocId, newRevId1, spaceId1 ),

			responseFilter = function( response )

				response = response:gsub( '<space1_spaceId>', space1.id )
				response = response:gsub( '<space1_youSpaceUserId>', space1.youSpaceUserId )

				return response

			end,

			headers = {
				status_code = '200',
			},
			
			options = {
				bypassRateLimiter = true,
			}
			
		},
		{
			url = string.format( "/v2/spaces/%s/updates?updated_since=pull2", spaceId1 ),

			response = string.format( [[{
				"base": "https://photos.adobe.io",
				"updates": [],
				"links": {
					"next": {
						"href": "/v2/spaces/%s/updates?updated_since=pull2"
					}
				}
			}]], spaceId1 ),

			headers = {
				status_code = '200',
			},

			options = {
				bypassRateLimiter = true,
			}

		},
		{
			url = "/v2/spaces/updates?updated_since=pull2",
			
			response = [[{
			  "base": "https://photos-stage.adobe.io/v2/",
			  "updates": [
				{
				  "created": "2019-08-21T17:01:02.467393Z",
				  "type": "space",
				  "subtype": "shared",
				  "id": "<space1_spaceId>",
				  "owner": {
					"id": "<space1_spaceOwnerId>",
					"outOfQuota": false,
					"entitlementExpired": false,
					"links": {
					  "self": {
						"href": "spaces/<space1_spaceId>/updates/<space1_spaceOwnerId>"
					  }
					}
				  },
				  "this_space_user": {
					"id": "<space1_youSpaceUserId>",
					"user": {
					  "id": "<space1_youFeedbackUserId>",
					  "links": {
						"self": {
						  "href": "spaces/<space1_spaceId>/updates/<space1_youFeedbackUserId>"
						}
					  }
					},
					"links": {
					  "self": {
						"href": "spaces/<space1_spaceId>/updates/<space1_youSpaceUserId>"
					  }
					}
				  },
				  "albums": [
					{
					  "id": "<album1_id>",
					  "links": {
						"self": {
						  "href": "spaces/<space1_spaceId>/updates/<album1_id>"
						}
					  },
					  "space_album": {
						"id": "<spaceAlbum1_id>",
						"links": {
						  "self": {
							"href": "spaces/<space1_spaceId>/updates/<spaceAlbum1_id>"
						  }
						}
					  }
					}
				  ],
				  "revision_ids": [
					"33ca44a7da51f56e6d3f9e80b9181aaf",
					"813254e958ea3ffac99d32cda7acc969",
					"b6088deb146acbbde9268c50156fa19a",
					"a0cb906593fa65713d198cec5407426a",
					"3a7c6f13acb6fd7afa9d979681ae1e20",
					"8f8b861e17b67454062ab16b7437b189",
					"2ddfea16ac1a4af1cd0b9a1937fc9c3a",
					"d86b4baa17e22e23bf1a87e84e7d9807"
				  ],
				  "updated": "2019-08-21T17:01:08.471614Z",
				  "country_blocked": false,
				  "payload": {
					"userUpdated": "2019-08-21T17:01:08.445000Z",
					"allowInviteRequests": true,
					"userCreated": "2019-08-21T17:01:02.439000Z",
					"presentationFilters": true,
					"location": true,
					"download": true,
					"private": true,
					"metadata": true
				  },
				  "createdOnClient": "Chrome-76.0.3809-MacOSX",
				  "links": {
					"self": {
					  "href": "spaces/<space1_spaceId>/updates/<space1_spaceId>"
					},
					"current": {
					  "href": "spaces/<space1_spaceId>/updates?updated_since=pull3"
					},
					"/rels/web_space": {
					  "href": "http://bit.ly/2ZkCLDo"
					},
					"start": {
					  "href": "spaces/<space1_spaceId>/updates"
					}
				  }
				}
			  ],
			  "links": {
				"next": {
				  "href": "/v2/spaces/updates?updated_since=pull2"
				}
			  }
			}]],
			
			responseFilter = function( response )

				response = response:gsub( '<space1_spaceId>', space1.id )
				response = response:gsub( '<space1_youSpaceUserId>', space1.youSpaceUserId )
				response = response:gsub( '<space1_spaceOwnerId>', space1.ownerId or KSIdGenerator.uuidInLowercaseHex() )
				response = response:gsub( '<space1_youFeedbackUserId>', space1.youFeedbackUserId or KSIdGenerator.uuidInLowercaseHex() ) 
				
				response = response:gsub( '<album1_id>', space1.albumId )
				response = response:gsub( '<spaceAlbum1_id>', space1.spaceAlbumId )

				return response

			end,
			
			headers = {
				status_code = '200',
			},
			
			options = {
				bypassRateLimiter = true,
			}
				
		},
		{
			url = string.format( "/v2/spaces/%s/updates?updated_since=pull2", spaceId1 ),
			
			response = string.format( [[{
				"base": "https://photos.adobe.io",
				"updates": [ {
					"updated": "2019-07-25T22:27:41.466149Z",
					  "type": "space_user",
					  "payload": {
						"status": "accepted",
						"role": "editor",
						"userUpdated": "2019-07-25T22:27:41.299863Z",
						"userCreated": "2019-07-25T22:27:41.299863Z"
					  },
					  "id": "<space1_youSpaceUserId>",
					  "user": {
						"id": "87bd59dd4c01e4f71155d7bfb50ccdf3"
					  },
					  "created": "2019-07-25T22:27:41.466149Z",
					  "revision_ids": [
						"ae43e159e80148fb47a8fe3ec4e6a718"
					  ]
					}
				],
				"links": {
					"next": {
						"href": "/v2/spaces/%s/updates?updated_since=pull3"
					}
				}
			}]], spaceId1 ),
			
			responseFilter = function( response )

				response = response:gsub( '<space1_spaceId>', space1.id )
				response = response:gsub( '<space1_youSpaceUserId>', space1.youSpaceUserId )

				return response

			end,

			headers = {
				status_code = '200',
			},
			
			options = {
				bypassRateLimiter = true,
			}
			
		},
		
		{
			url = string.format( "/v2/spaces/%s/updates?updated_since=pull3", spaceId1 ),

			response = string.format( [[{
				"base": "https://photos.adobe.io",
				"updates": [],
				"links": {
					"next": {
						"href": "/v2/spaces/%s/updates?updated_since=pull3"
					}
				}
			}]], spaceId1 ),

			headers = {
				status_code = '200',
			},

			options = {
				bypassRateLimiter = true,
			}

		},

	}
	
	local args = {
		spaceUser1Role = 'viewer'
	}
	
	local catalogSpec = createCatalogSpecForTest( rxTest, space1, space2, expectedResponses, args )

	local wfCatalog = createWFCatalogForTestWithSpec( rxTest, catalogSpec )
	
	local errorHandler = WFCatErrorHandler( wfCatalog,{
		deleteAssets = bulkDelete,
		deleteIndividualAlbum = deleteDoc },
		{ debugMode = true } )
	
	rxTest:addCloseTarget( errorHandler )
	
	local spaceFeedsManagerArgs = {
		taskDelays = {
			progressiveBackoff = false,
			noWorkDelay = 0.5,
			maxNoWorkDelay = 1,
			errorDelay = 1,
			maxDelay = 1,
		},
		albumAssetsForFilteredAssetsQueryFn = albumAssetsForFilteredAssetsQueryFn,
		testMode = true
	}

	-- Add an asset to the space.

	local assetDoc = buildDocValue( rxTest, wfCatalog, assetDocId )
	assetDoc._spaceId = spaceId1
	assetDoc._newRevId = newRevId1
	assetDoc.develop = nil
	KSTableUtils.setNested( assetDoc, captureDate, 'captureDate' )
	
	rxTest:expectSingleValue( wfCatalog:docStore():setDocValue( assetDoc ), ':ignore:' )

	-- Start up the space feed manager

	local spaceFeedsManager = WFCatSpaceFeedsManager( wfCatalog, spaceFeedsManagerArgs )
	rxTest:addCloseTarget( spaceFeedsManager )
	
	-- Verify the active spaces.
	
	local activeSpacesObserver = rxTest:observe( spaceFeedsManager._activeSpacesStream )
	
	local isDownloadingObserver = rxTest:observe( spaceFeedsManager:isDownloadingMetadata() )
	
	rxTest:expectOnlyCompleted( spaceFeedsManager:_testOnly_getUpdatesOnce() )
	
	activeSpacesObserver:expectDataWithinTime( 2, spaceId1, spaceUserId1, ":ignore:", false, ":ignore:", ":ignore:" )
	activeSpacesObserver:expectDataWithinTime( 2, spaceId2, spaceUserId2, ":ignore:", false, ":ignore:", ":ignore:" )
	
	isDownloadingObserver:expectDataWithinTime( 5, true ) -- Space 1
	isDownloadingObserver:expectDataWithinTime( 5, false )
	
	isDownloadingObserver:expectDataWithinTime( 5, true ) -- Space 2
	isDownloadingObserver:expectDataWithinTime( 5, false )
	
	isDownloadingObserver:dispose()
	
	-- Trigger the space metadata pusher.
	
	local sendNUpdates = rxTest:observe( spaceFeedsManager._spacePusher:_DEBUG_sendUpdatesOnce() )
	sendNUpdates:expectCompletedWithinTime( 5 )
	sendNUpdates:dispose()
	
	-- Get more updates. This should fetch the error doc created from attempting
	-- to push the update.
	
	rxTest:expectOnlyCompleted( spaceFeedsManager:_testOnly_getUpdatesOnce() )
	
	-- Verify the asset doc is marked with the insufficient permission error.
	
	local errorObserver = rxTest:observe( wfCatalog:docStore():ongoingDocValue( assetDocId )
			:map( function( docValue )
				return KSTableUtils.derefOpt( docValue, '_localOnly', 'ozError', 'subtype' )
			end ) )
			
	errorObserver:expectDataWithinTime( 5, 'InsufficientPermissionError' )

	-- Get more updates. This should update spaceUser1 to a editor
	
	rxTest:expectOnlyCompleted( spaceFeedsManager:_testOnly_getUpdatesOnce() )
	
	activeSpacesObserver:expectDataWithinTime( 2, spaceId1, spaceUserId1, ":ignore:", false, ":ignore:", ":ignore:" )
	
	activeSpacesObserver:dispose()
	
	-- Verify the asset doc is no longer marked as having an error.
			
	errorObserver:expectDataWithinTime( 5, nil )
	
	errorObserver:dispose()
	
end

--------------------------------------------------------------------------------

function WFCatSpaceFeedsManagerTest.rxTestInsufficientPermission_ContributorToEditor( rxTest )

	rxTest:setTimeLimit( 30 )

	local assetDocId = '44ab164cd874418ca566d5a0e259ac0d'
	local newRevId1 =  'rrrr164cd874418ca566d5a0e259ac0d'
	local captureDate = '2019-09-26T18:13:30.050Z'
	
	local expectedResponses = {
		{
			url = string.format( "/v2/spaces/%s/updates", spaceId1 ),

			response = string.format( [[{
				"base": "https://photos.adobe.io",
				"updates": [ {
						"id": "8462f5ba3b39fd050d3cd6b7352ba561",
						"created": "2015-05-27T00:34:56.669987Z",
						"updated": "2015-05-27T00:38:29.160369Z",
						"type": "asset",
						"subtype": "image",
						"revision_ids": [
							"a1e880f718a348b89663cf38f9f29d23",
							"cd1ab30cf8464ef2a9e736b50c0ba33d",
							"96f33a7215e04c9aa49346bbf8aae921",
							"813fdbda1b244c2d9f41008ba63eedba"
						],
						"payload": {
							"importSource": {
								"importTimestamp": "2015-05-26T17:34:23-07:00",
								"originalHeight": 1600,
								"originalWidth": 1068
							},
							"captureDate": "2015-05-17T15:50:12",
							"xmp": {
								"dc": {
									"rights": "test copyright string"
								}
							},
							"location": {
								"longitude": -93.600107,
								"latitude": 45.951733,
								"altitude": 377.9
							}
						}
					}
				],
				"links": {
					"next": {
						"href": "/v2/spaces/%s/updates?updated_since=pull1"
					}
				}
			}]], spaceId1 ),

			headers = {
				status_code = '200',
			},

			options = {
				bypassRateLimiter = true,
			}

		},
		{
			url = string.format( "/v2/spaces/%s/updates?updated_since=pull1", spaceId1 ),

			response = string.format( [[{
				"base": "https://photos.adobe.io",
				"updates": [],
				"links": {
					"next": {
						"href": "/v2/spaces/%s/updates?updated_since=pull1"
					}
				}
			}]], spaceId1 ),

			headers = {
				status_code = '200',
			},

			options = {
				bypassRateLimiter = true,
			}

		},
		{
			url = string.format( "/v2/spaces/%s/updates", spaceId2 ),

			response = string.format( [[{
				"base": "https://photos.adobe.io",
				"updates": [ {
						"id": "2222f5ba3b39fd050d3cd6b7352ba561",
						"created": "2015-05-27T00:34:56.669987Z",
						"updated": "2015-05-27T00:38:29.160369Z",
						"type": "asset",
						"subtype": "image",
						"revision_ids": [
							"22e880f718a348b89663cf38f9f29d23",
							"331ab30cf8464ef2a9e736b50c0ba33d"
						],
						"payload": {
							"importSource": {
								"importTimestamp": "2015-05-26T17:34:23-07:00",
								"originalHeight": 1600,
								"originalWidth": 1068
							},
							"captureDate": "2015-05-17T15:50:12",
							"xmp": {
								"dc": {
									"rights": "test copyright string"
								}
							},
							"location": {
								"longitude": -93.600107,
								"latitude": 45.951733,
								"altitude": 377.9
							}
						}
					}
				],
				"links": {
					"next": {
						"href": "/v2/spaces/%s/updates?updated_since=pull1"
					}
				}
			}]], spaceId2 ),

			headers = {
				status_code = '200',
			},

			options = {
				bypassRateLimiter = true,
			}

		},
		{
			url = string.format( "/v2/spaces/%s/updates?updated_since=pull1", spaceId2 ),

			response = string.format( [[{
				"base": "https://photos.adobe.io",
				"updates": [],
				"links": {
					"next": {
						"href": "/v2/spaces/%s/updates?updated_since=pull1"
					}
				}
			}]], spaceId2 ),

			headers = {
				status_code = '200',
			},

			options = {
				bypassRateLimiter = true,
			}

		},
		{
			url = string.format( "/v2/spaces/updates" ),
				
			data = string.format( [[[{"space_id":"%s","type":"asset","action":"create","revision_id":"%s","id":"%s","payload":{"importSource":{},"captureDate":"%s"},"subtype":"image"}] ]], 
					spaceId1, newRevId1, assetDocId, captureDate ),
			
			dataFilter = function( str )
			
				str = str:gsub( '"sha256":"[0-9a-f]+"', "" ) .. ' '

				return str
				
			end,
		
			options = {
				requestHeaders = {
					{
						header = "Content-Type",
						value = "application/json",
					},
					{
						header = "Accept",
						value = "application/json",
					},
				},
				requestMethod = "POST",
				bypassRateLimiter = true,
			},

		},
		{
			url = "/v2/spaces/updates?updated_since=pull1",
			
			response = [[{
			  "base": "https://photos-stage.adobe.io/v2/",
			  "updates": [
				{
				  "created": "2019-08-21T17:01:02.467393Z",
				  "type": "space",
				  "subtype": "shared",
				  "id": "<space1_spaceId>",
				  "owner": {
					"id": "<space1_spaceOwnerId>",
					"outOfQuota": false,
					"entitlementExpired": false,
					"links": {
					  "self": {
						"href": "spaces/<space1_spaceId>/updates/<space1_spaceOwnerId>"
					  }
					}
				  },
				  "this_space_user": {
					"id": "<space1_youSpaceUserId>",
					"user": {
					  "id": "<space1_youFeedbackUserId>",
					  "links": {
						"self": {
						  "href": "spaces/<space1_spaceId>/updates/<space1_youFeedbackUserId>"
						}
					  }
					},
					"links": {
					  "self": {
						"href": "spaces/<space1_spaceId>/updates/<space1_youSpaceUserId>"
					  }
					}
				  },
				  "albums": [
					{
					  "id": "<album1_id>",
					  "links": {
						"self": {
						  "href": "spaces/<space1_spaceId>/updates/<album1_id>"
						}
					  },
					  "space_album": {
						"id": "<spaceAlbum1_id>",
						"links": {
						  "self": {
							"href": "spaces/<space1_spaceId>/updates/<spaceAlbum1_id>"
						  }
						}
					  }
					}
				  ],
				  "revision_ids": [
					"33ca44a7da51f56e6d3f9e80b9181aaf",
					"813254e958ea3ffac99d32cda7acc969",
					"b6088deb146acbbde9268c50156fa19a",
					"a0cb906593fa65713d198cec5407426a",
					"3a7c6f13acb6fd7afa9d979681ae1e20",
					"8f8b861e17b67454062ab16b7437b189",
					"2ddfea16ac1a4af1cd0b9a1937fc9c3a",
					"d86b4baa17e22e23bf1a87e84e7d9807",
					"356b4baa17e22e23bf1a87e84e7d9807"
				  ],
				  "updated": "2019-08-22T17:01:08.471614Z",
				  "country_blocked": false,
				  "payload": {
					"userUpdated": "2019-08-21T17:01:08.445000Z",
					"allowInviteRequests": true,
					"userCreated": "2019-08-21T17:01:02.439000Z",
					"presentationFilters": true,
					"location": true,
					"download": true,
					"private": true,
					"metadata": true
				  },
				  "createdOnClient": "Chrome-76.0.3809-MacOSX",
				  "links": {
					"self": {
					  "href": "spaces/<space1_spaceId>/updates/<space1_spaceId>"
					},
					"current": {
					  "href": "spaces/<space1_spaceId>/updates?updated_since=pull2"
					},
					"/rels/web_space": {
					  "href": "http://bit.ly/2ZkCLDo"
					},
					"start": {
					  "href": "spaces/<space1_spaceId>/updates"
					}
				  }
				}
			  ],
			  "links": {
				"next": {
				  "href": "/v2/spaces/updates?updated_since=pull2"
				}
			  }
			}]],
			
			responseFilter = function( response )

				response = response:gsub( '<space1_spaceId>', space1.id )
				response = response:gsub( '<space1_youSpaceUserId>', space1.youSpaceUserId )
				response = response:gsub( '<space1_spaceOwnerId>', space1.ownerId or KSIdGenerator.uuidInLowercaseHex() )
				response = response:gsub( '<space1_youFeedbackUserId>', space1.youFeedbackUserId or KSIdGenerator.uuidInLowercaseHex() ) 
				
				response = response:gsub( '<album1_id>', space1.albumId )
				response = response:gsub( '<spaceAlbum1_id>', space1.spaceAlbumId )

				return response

			end,
			
			headers = {
				status_code = '200',
			},
			
			options = {
				bypassRateLimiter = true,
			}
				
		},
		{
			url = string.format( "/v2/spaces/%s/updates?updated_since=pull1", spaceId1 ),
			
			response = string.format( [[{
				"base": "https://photos.adobe.io",
				"updates": [ {
					  "updated": "2019-09-26T21:20:54.529292Z",
					  "type": "error",
					  "source": {
						"id": "%s",
						"type": "asset",
						"action": "create",
						"revision_id": "%s"
					  },
					  "subtype": "InsufficientPermissionError",
					  "id": "08eab133e5ba2161393aed62bee916d7",
					  "errors": {},
					  "status": 403
					}
				],
				"links": {
					"next": {
						"href": "/v2/spaces/%s/updates?updated_since=pull2"
					}
				}
			}]], assetDocId, newRevId1, spaceId1 ),

			responseFilter = function( response )

				response = response:gsub( '<space1_spaceId>', space1.id )
				response = response:gsub( '<space1_youSpaceUserId>', space1.youSpaceUserId )

				return response

			end,

			headers = {
				status_code = '200',
			},
			
			options = {
				bypassRateLimiter = true,
			}
			
		},
		{
			url = string.format( "/v2/spaces/%s/updates?updated_since=pull2", spaceId1 ),

			response = string.format( [[{
				"base": "https://photos.adobe.io",
				"updates": [],
				"links": {
					"next": {
						"href": "/v2/spaces/%s/updates?updated_since=pull2"
					}
				}
			}]], spaceId1 ),

			headers = {
				status_code = '200',
			},

			options = {
				bypassRateLimiter = true,
			}

		},
		{
			url = "/v2/spaces/updates?updated_since=pull2",
			
			response = [[{
			  "base": "https://photos-stage.adobe.io/v2/",
			  "updates": [
				{
				  "created": "2019-08-21T17:01:02.467393Z",
				  "type": "space",
				  "subtype": "shared",
				  "id": "<space1_spaceId>",
				  "owner": {
					"id": "<space1_spaceOwnerId>",
					"outOfQuota": false,
					"entitlementExpired": false,
					"links": {
					  "self": {
						"href": "spaces/<space1_spaceId>/updates/<space1_spaceOwnerId>"
					  }
					}
				  },
				  "this_space_user": {
					"id": "<space1_youSpaceUserId>",
					"user": {
					  "id": "<space1_youFeedbackUserId>",
					  "links": {
						"self": {
						  "href": "spaces/<space1_spaceId>/updates/<space1_youFeedbackUserId>"
						}
					  }
					},
					"links": {
					  "self": {
						"href": "spaces/<space1_spaceId>/updates/<space1_youSpaceUserId>"
					  }
					}
				  },
				  "albums": [
					{
					  "id": "<album1_id>",
					  "links": {
						"self": {
						  "href": "spaces/<space1_spaceId>/updates/<album1_id>"
						}
					  },
					  "space_album": {
						"id": "<spaceAlbum1_id>",
						"links": {
						  "self": {
							"href": "spaces/<space1_spaceId>/updates/<spaceAlbum1_id>"
						  }
						}
					  }
					}
				  ],
				  "revision_ids": [
					"33ca44a7da51f56e6d3f9e80b9181aaf",
					"813254e958ea3ffac99d32cda7acc969",
					"b6088deb146acbbde9268c50156fa19a",
					"a0cb906593fa65713d198cec5407426a",
					"3a7c6f13acb6fd7afa9d979681ae1e20",
					"8f8b861e17b67454062ab16b7437b189",
					"2ddfea16ac1a4af1cd0b9a1937fc9c3a",
					"d86b4baa17e22e23bf1a87e84e7d9807"
				  ],
				  "updated": "2019-08-21T17:01:08.471614Z",
				  "country_blocked": false,
				  "payload": {
					"userUpdated": "2019-08-21T17:01:08.445000Z",
					"allowInviteRequests": true,
					"userCreated": "2019-08-21T17:01:02.439000Z",
					"presentationFilters": true,
					"location": true,
					"download": true,
					"private": true,
					"metadata": true
				  },
				  "createdOnClient": "Chrome-76.0.3809-MacOSX",
				  "links": {
					"self": {
					  "href": "spaces/<space1_spaceId>/updates/<space1_spaceId>"
					},
					"current": {
					  "href": "spaces/<space1_spaceId>/updates?updated_since=pull3"
					},
					"/rels/web_space": {
					  "href": "http://bit.ly/2ZkCLDo"
					},
					"start": {
					  "href": "spaces/<space1_spaceId>/updates"
					}
				  }
				}
			  ],
			  "links": {
				"next": {
				  "href": "/v2/spaces/updates?updated_since=pull2"
				}
			  }
			}]],
			
			responseFilter = function( response )

				response = response:gsub( '<space1_spaceId>', space1.id )
				response = response:gsub( '<space1_youSpaceUserId>', space1.youSpaceUserId )
				response = response:gsub( '<space1_spaceOwnerId>', space1.ownerId or KSIdGenerator.uuidInLowercaseHex() )
				response = response:gsub( '<space1_youFeedbackUserId>', space1.youFeedbackUserId or KSIdGenerator.uuidInLowercaseHex() ) 
				
				response = response:gsub( '<album1_id>', space1.albumId )
				response = response:gsub( '<spaceAlbum1_id>', space1.spaceAlbumId )

				return response

			end,
			
			headers = {
				status_code = '200',
			},
			
			options = {
				bypassRateLimiter = true,
			}
				
		},
		{
			url = string.format( "/v2/spaces/%s/updates?updated_since=pull2", spaceId1 ),
			
			response = string.format( [[{
				"base": "https://photos.adobe.io",
				"updates": [ {
					"updated": "2019-07-25T22:27:41.466149Z",
					  "type": "space_user",
					  "payload": {
						"status": "accepted",
						"role": "editor",
						"userUpdated": "2019-07-25T22:27:41.299863Z",
						"userCreated": "2019-07-25T22:27:41.299863Z"
					  },
					  "id": "<space1_youSpaceUserId>",
					  "user": {
						"id": "87bd59dd4c01e4f71155d7bfb50ccdf3"
					  },
					  "created": "2019-07-25T22:27:41.466149Z",
					  "revision_ids": [
						"ae43e159e80148fb47a8fe3ec4e6a718"
					  ]
					}
				],
				"links": {
					"next": {
						"href": "/v2/spaces/%s/updates?updated_since=pull3"
					}
				}
			}]], spaceId1 ),
			
			responseFilter = function( response )

				response = response:gsub( '<space1_spaceId>', space1.id )
				response = response:gsub( '<space1_youSpaceUserId>', space1.youSpaceUserId )

				return response

			end,

			headers = {
				status_code = '200',
			},
			
			options = {
				bypassRateLimiter = true,
			}
			
		},
		
		{
			url = string.format( "/v2/spaces/%s/updates?updated_since=pull3", spaceId1 ),

			response = string.format( [[{
				"base": "https://photos.adobe.io",
				"updates": [],
				"links": {
					"next": {
						"href": "/v2/spaces/%s/updates?updated_since=pull3"
					}
				}
			}]], spaceId1 ),

			headers = {
				status_code = '200',
			},

			options = {
				bypassRateLimiter = true,
			}

		},

	}
	
	local args = {
		spaceUser1Role = 'contributor'
	}
	
	local catalogSpec = createCatalogSpecForTest( rxTest, space1, space2, expectedResponses, args )

	local wfCatalog = createWFCatalogForTestWithSpec( rxTest, catalogSpec )
	
	local errorHandler = WFCatErrorHandler( wfCatalog,{
		deleteAssets = bulkDelete,
		deleteIndividualAlbum = deleteDoc },
		{ debugMode = true } )
	
	rxTest:addCloseTarget( errorHandler )
	
	local spaceFeedsManagerArgs = {
		taskDelays = {
			progressiveBackoff = false,
			noWorkDelay = 0.5,
			maxNoWorkDelay = 1,
			errorDelay = 1,
			maxDelay = 1,
		},
		albumAssetsForFilteredAssetsQueryFn = albumAssetsForFilteredAssetsQueryFn,
		testMode = true
	}

	-- Add an asset to the space.

	local assetDoc = buildDocValue( rxTest, wfCatalog, assetDocId )
	assetDoc._spaceId = spaceId1
	assetDoc._newRevId = newRevId1
	assetDoc.develop = nil
	KSTableUtils.setNested( assetDoc, captureDate, 'captureDate' )
	
	rxTest:expectSingleValue( wfCatalog:docStore():setDocValue( assetDoc ), ':ignore:' )

	-- Start up the space feed manager

	local spaceFeedsManager = WFCatSpaceFeedsManager( wfCatalog, spaceFeedsManagerArgs )
	rxTest:addCloseTarget( spaceFeedsManager )
	
	-- Verify the active spaces.
	
	local activeSpacesObserver = rxTest:observe( spaceFeedsManager._activeSpacesStream )
	
	local isDownloadingObserver = rxTest:observe( spaceFeedsManager:isDownloadingMetadata() )
	
	rxTest:expectOnlyCompleted( spaceFeedsManager:_testOnly_getUpdatesOnce() )
	
	activeSpacesObserver:expectDataWithinTime( 2, spaceId1, spaceUserId1, ":ignore:", false, ":ignore:", ":ignore:" )
	activeSpacesObserver:expectDataWithinTime( 2, spaceId2, spaceUserId2, ":ignore:", false, ":ignore:", ":ignore:" )
	
	isDownloadingObserver:expectDataWithinTime( 5, true ) -- Space 1
	isDownloadingObserver:expectDataWithinTime( 5, false )
	
	isDownloadingObserver:expectDataWithinTime( 5, true ) -- Space 2
	isDownloadingObserver:expectDataWithinTime( 5, false )
	
	isDownloadingObserver:dispose()
	
	-- Trigger the space metadata pusher.
	
	local sendNUpdates = rxTest:observe( spaceFeedsManager._spacePusher:_DEBUG_sendUpdatesOnce() )
	sendNUpdates:expectCompletedWithinTime( 5 )
	sendNUpdates:dispose()
	
	-- Get more updates. This should fetch the error doc created from attempting
	-- to push the update.
	
	rxTest:expectOnlyCompleted( spaceFeedsManager:_testOnly_getUpdatesOnce() )
	
	-- Verify the asset doc is marked with the insufficient permission error.
	
	local errorObserver = rxTest:observe( wfCatalog:docStore():ongoingDocValue( assetDocId )
			:map( function( docValue )
				return KSTableUtils.derefOpt( docValue, '_localOnly', 'ozError', 'subtype' )
			end ) )
			
	errorObserver:expectDataWithinTime( 5, 'InsufficientPermissionError' )

	-- Get more updates. This should update spaceUser1 to a editor
	
	rxTest:expectOnlyCompleted( spaceFeedsManager:_testOnly_getUpdatesOnce() )
	
	activeSpacesObserver:expectDataWithinTime( 2, spaceId1, spaceUserId1, ":ignore:", false, ":ignore:", ":ignore:" )
	
	activeSpacesObserver:dispose()
	
	-- Verify the asset doc is no longer marked as having an error.
			
	errorObserver:expectDataWithinTime( 5, nil )
	
	errorObserver:dispose()
	
end

--------------------------------------------------------------------------------

function WFCatSpaceFeedsManagerTest.rxTestInsufficientPermission_Entitlement( rxTest )

	rxTest:setTimeLimit( 30 )

	local assetDocId = '44ab164cd874418ca566d5a0e259ac0d'
	local newRevId1 =  'rrrr164cd874418ca566d5a0e259ac0d'
	local captureDate = '2019-09-26T18:13:30.050Z'
	
	local expectedResponses = {
		{
			url = string.format( "/v2/spaces/%s/updates", spaceId1 ),

			response = string.format( [[{
				"base": "https://photos.adobe.io",
				"updates": [ {
						"id": "8462f5ba3b39fd050d3cd6b7352ba561",
						"created": "2015-05-27T00:34:56.669987Z",
						"updated": "2015-05-27T00:38:29.160369Z",
						"type": "asset",
						"subtype": "image",
						"revision_ids": [
							"a1e880f718a348b89663cf38f9f29d23",
							"cd1ab30cf8464ef2a9e736b50c0ba33d",
							"96f33a7215e04c9aa49346bbf8aae921",
							"813fdbda1b244c2d9f41008ba63eedba"
						],
						"payload": {
							"importSource": {
								"importTimestamp": "2015-05-26T17:34:23-07:00",
								"originalHeight": 1600,
								"originalWidth": 1068
							},
							"captureDate": "2015-05-17T15:50:12",
							"xmp": {
								"dc": {
									"rights": "test copyright string"
								}
							},
							"location": {
								"longitude": -93.600107,
								"latitude": 45.951733,
								"altitude": 377.9
							}
						}
					}
				],
				"links": {
					"next": {
						"href": "/v2/spaces/%s/updates?updated_since=pull1"
					}
				}
			}]], spaceId1 ),

			headers = {
				status_code = '200',
			},

			options = {
				bypassRateLimiter = true,
			}

		},
		{
			url = string.format( "/v2/spaces/%s/updates?updated_since=pull1", spaceId1 ),

			response = string.format( [[{
				"base": "https://photos.adobe.io",
				"updates": [],
				"links": {
					"next": {
						"href": "/v2/spaces/%s/updates?updated_since=pull1"
					}
				}
			}]], spaceId1 ),

			headers = {
				status_code = '200',
			},

			options = {
				bypassRateLimiter = true,
			}

		},
		{
			url = string.format( "/v2/spaces/%s/updates", spaceId2 ),

			response = string.format( [[{
				"base": "https://photos.adobe.io",
				"updates": [ {
						"id": "2222f5ba3b39fd050d3cd6b7352ba561",
						"created": "2015-05-27T00:34:56.669987Z",
						"updated": "2015-05-27T00:38:29.160369Z",
						"type": "asset",
						"subtype": "image",
						"revision_ids": [
							"22e880f718a348b89663cf38f9f29d23",
							"331ab30cf8464ef2a9e736b50c0ba33d"
						],
						"payload": {
							"importSource": {
								"importTimestamp": "2015-05-26T17:34:23-07:00",
								"originalHeight": 1600,
								"originalWidth": 1068
							},
							"captureDate": "2015-05-17T15:50:12",
							"xmp": {
								"dc": {
									"rights": "test copyright string"
								}
							},
							"location": {
								"longitude": -93.600107,
								"latitude": 45.951733,
								"altitude": 377.9
							}
						}
					}
				],
				"links": {
					"next": {
						"href": "/v2/spaces/%s/updates?updated_since=pull1"
					}
				}
			}]], spaceId2 ),

			headers = {
				status_code = '200',
			},

			options = {
				bypassRateLimiter = true,
			}

		},
		{
			url = string.format( "/v2/spaces/%s/updates?updated_since=pull1", spaceId2 ),

			response = string.format( [[{
				"base": "https://photos.adobe.io",
				"updates": [],
				"links": {
					"next": {
						"href": "/v2/spaces/%s/updates?updated_since=pull1"
					}
				}
			}]], spaceId2 ),

			headers = {
				status_code = '200',
			},

			options = {
				bypassRateLimiter = true,
			}

		},
		{
			url = string.format( "/v2/spaces/updates" ),
				
			data = string.format( [[[{"space_id":"%s","type":"asset","action":"create","revision_id":"%s","id":"%s","payload":{"importSource":{},"captureDate":"%s"},"subtype":"image"}] ]], 
					spaceId1, newRevId1, assetDocId, captureDate ),
			
			dataFilter = function( str )
			
				str = str:gsub( '"sha256":"[0-9a-f]+"', "" ) .. ' '

				return str
				
			end,
		
			options = {
				requestHeaders = {
					{
						header = "Content-Type",
						value = "application/json",
					},
					{
						header = "Accept",
						value = "application/json",
					},
				},
				requestMethod = "POST",
				bypassRateLimiter = true,
			},

		},
		{
			url = "/v2/spaces/updates?updated_since=pull1",
			
			response = [[{
			  "base": "https://photos-stage.adobe.io/v2/",
			  "updates": [
				{
				  "created": "2019-08-21T17:01:02.467393Z",
				  "type": "space",
				  "subtype": "shared",
				  "id": "<space1_spaceId>",
				  "owner": {
					"id": "<space1_spaceOwnerId>",
					"outOfQuota": false,
					"entitlementExpired": false,
					"links": {
					  "self": {
						"href": "spaces/<space1_spaceId>/updates/<space1_spaceOwnerId>"
					  }
					}
				  },
				  "this_space_user": {
					"id": "<space1_youSpaceUserId>",
					"user": {
					  "id": "<space1_youFeedbackUserId>",
					  "links": {
						"self": {
						  "href": "spaces/<space1_spaceId>/updates/<space1_youFeedbackUserId>"
						}
					  }
					},
					"links": {
					  "self": {
						"href": "spaces/<space1_spaceId>/updates/<space1_youSpaceUserId>"
					  }
					}
				  },
				  "albums": [
					{
					  "id": "<album1_id>",
					  "links": {
						"self": {
						  "href": "spaces/<space1_spaceId>/updates/<album1_id>"
						}
					  },
					  "space_album": {
						"id": "<spaceAlbum1_id>",
						"links": {
						  "self": {
							"href": "spaces/<space1_spaceId>/updates/<spaceAlbum1_id>"
						  }
						}
					  }
					}
				  ],
				  "revision_ids": [
					"33ca44a7da51f56e6d3f9e80b9181aaf",
					"813254e958ea3ffac99d32cda7acc969",
					"b6088deb146acbbde9268c50156fa19a",
					"a0cb906593fa65713d198cec5407426a",
					"3a7c6f13acb6fd7afa9d979681ae1e20",
					"8f8b861e17b67454062ab16b7437b189",
					"2ddfea16ac1a4af1cd0b9a1937fc9c3a",
					"d86b4baa17e22e23bf1a87e84e7d9807",
					"356b4baa17e22e23bf1a87e84e7d9807"
				  ],
				  "updated": "2019-08-22T17:01:08.471614Z",
				  "country_blocked": false,
				  "payload": {
					"userUpdated": "2019-08-21T17:01:08.445000Z",
					"allowInviteRequests": true,
					"userCreated": "2019-08-21T17:01:02.439000Z",
					"presentationFilters": true,
					"location": true,
					"download": true,
					"private": true,
					"metadata": true
				  },
				  "createdOnClient": "Chrome-76.0.3809-MacOSX",
				  "links": {
					"self": {
					  "href": "spaces/<space1_spaceId>/updates/<space1_spaceId>"
					},
					"current": {
					  "href": "spaces/<space1_spaceId>/updates?updated_since=pull2"
					},
					"/rels/web_space": {
					  "href": "http://bit.ly/2ZkCLDo"
					},
					"start": {
					  "href": "spaces/<space1_spaceId>/updates"
					}
				  }
				}
			  ],
			  "links": {
				"next": {
				  "href": "/v2/spaces/updates?updated_since=pull2"
				}
			  }
			}]],
			
			responseFilter = function( response )

				response = response:gsub( '<space1_spaceId>', space1.id )
				response = response:gsub( '<space1_youSpaceUserId>', space1.youSpaceUserId )
				response = response:gsub( '<space1_spaceOwnerId>', space1.ownerId or KSIdGenerator.uuidInLowercaseHex() )
				response = response:gsub( '<space1_youFeedbackUserId>', space1.youFeedbackUserId or KSIdGenerator.uuidInLowercaseHex() ) 
				
				response = response:gsub( '<album1_id>', space1.albumId )
				response = response:gsub( '<spaceAlbum1_id>', space1.spaceAlbumId )

				return response

			end,
			
			headers = {
				status_code = '200',
			},
			
			options = {
				bypassRateLimiter = true,
			}
				
		},
		{
			url = string.format( "/v2/spaces/%s/updates?updated_since=pull1", spaceId1 ),
			
			response = string.format( [[{
				"base": "https://photos.adobe.io",
				"updates": [ {
					  "updated": "2019-09-26T21:20:54.529292Z",
					  "type": "error",
					  "source": {
						"id": "%s",
						"type": "asset",
						"action": "create",
						"revision_id": "%s"
					  },
					  "subtype": "InsufficientPermissionError",
					  "id": "08eab133e5ba2161393aed62bee916d7",
					  "errors": {},
					  "status": 403
					}
				],
				"links": {
					"next": {
						"href": "/v2/spaces/%s/updates?updated_since=pull2"
					}
				}
			}]], assetDocId, newRevId1, spaceId1 ),

			responseFilter = function( response )

				response = response:gsub( '<space1_spaceId>', space1.id )
				response = response:gsub( '<space1_youSpaceUserId>', space1.youSpaceUserId )

				return response

			end,

			headers = {
				status_code = '200',
			},
			
			options = {
				bypassRateLimiter = true,
			}
			
		},
		{
			url = string.format( "/v2/spaces/%s/updates?updated_since=pull2", spaceId1 ),

			response = string.format( [[{
				"base": "https://photos.adobe.io",
				"updates": [],
				"links": {
					"next": {
						"href": "/v2/spaces/%s/updates?updated_since=pull2"
					}
				}
			}]], spaceId1 ),

			headers = {
				status_code = '200',
			},

			options = {
				bypassRateLimiter = true,
			}

		},
	}
	
	local args = {
		spaceUser1Role = 'editor'
	}
	
	local catalogSpec = createCatalogSpecForTest( rxTest, space1, space2, expectedResponses, args )

	local wfCatalog = createWFCatalogForTestWithSpec( rxTest, catalogSpec )
	
	local errorHandler = WFCatErrorHandler( wfCatalog,{
		deleteAssets = bulkDelete,
		deleteIndividualAlbum = deleteDoc },
		{ debugMode = true } )
	
	rxTest:addCloseTarget( errorHandler )
	
	local spaceFeedsManagerArgs = {
		taskDelays = {
			progressiveBackoff = false,
			noWorkDelay = 0.5,
			maxNoWorkDelay = 1,
			errorDelay = 1,
			maxDelay = 1,
		},
		albumAssetsForFilteredAssetsQueryFn = albumAssetsForFilteredAssetsQueryFn,
		testMode = true
	}

	-- Add an asset to the space.

	local assetDoc = buildDocValue( rxTest, wfCatalog, assetDocId )
	assetDoc._spaceId = spaceId1
	assetDoc._newRevId = newRevId1
	assetDoc.develop = nil
	KSTableUtils.setNested( assetDoc, captureDate, 'captureDate' )
	
	rxTest:expectSingleValue( wfCatalog:docStore():setDocValue( assetDoc ), ':ignore:' )

	-- Start up the space feed manager

	local spaceFeedsManager = WFCatSpaceFeedsManager( wfCatalog, spaceFeedsManagerArgs )
	rxTest:addCloseTarget( spaceFeedsManager )
	
	-- Verify the active spaces.
	
	local activeSpacesObserver = rxTest:observe( spaceFeedsManager._activeSpacesStream )
	
	local isDownloadingObserver = rxTest:observe( spaceFeedsManager:isDownloadingMetadata() )
	
	rxTest:expectOnlyCompleted( spaceFeedsManager:_testOnly_getUpdatesOnce() )
	
	activeSpacesObserver:expectDataWithinTime( 2, spaceId1, spaceUserId1, ":ignore:", false, ":ignore:", ":ignore:" )
	activeSpacesObserver:expectDataWithinTime( 2, spaceId2, spaceUserId2, ":ignore:", false, ":ignore:", ":ignore:" )
	
	isDownloadingObserver:expectDataWithinTime( 5, true ) -- Space 1
	isDownloadingObserver:expectDataWithinTime( 5, false )
	
	isDownloadingObserver:expectDataWithinTime( 5, true ) -- Space 2
	isDownloadingObserver:expectDataWithinTime( 5, false )
	
	isDownloadingObserver:dispose()
	
	-- Trigger the space metadata pusher.
	
	local sendNUpdates = rxTest:observe( spaceFeedsManager._spacePusher:_DEBUG_sendUpdatesOnce() )
	sendNUpdates:expectCompletedWithinTime( 5 )
	sendNUpdates:dispose()
	
	-- Get more updates. This should fetch the error doc created from attempting
	-- to push the update.
	
	rxTest:expectOnlyCompleted( spaceFeedsManager:_testOnly_getUpdatesOnce() )
	
	-- Verify the asset doc is marked with the insufficient permission error.
	
	local errorObserver = rxTest:observe( wfCatalog:docStore():ongoingDocValue( assetDocId )
			:map( function( docValue )
				return KSTableUtils.derefOpt( docValue, '_localOnly', 'ozError', 'subtype' )
			end ) )
			
	errorObserver:expectDataWithinTime( 5, 'InsufficientPermissionError' )

	-- Get more updates. This should update spaceUser1 to a editor
	
	catalogSpec:updateEntitlementStatus( 'trial' )
	
	-- Verify the asset doc is no longer marked as having an error.
			
	errorObserver:expectDataWithinTime( 5, nil )
	
	errorObserver:dispose()
	
end

--------------------------------------------------------------------------------

function WFCatSpaceFeedsManagerTest.rxTestInsufficientPermission_PendingEdit( rxTest )

	-- The following test verifies the following workflow:
	
	-- 1. Alice grants Bob editor role in a shared album containing SomeImage.
	-- 2. Bob syncs down SomeImage and goes offline where he makes edits.
	-- 3. In the meantime, Alice downgrades Bob's privileges SomeImage album to contributor while Bob is offline.
	-- 4. Bob comes back online
	
	-- After Bob comes back online, changes to the asset are discarded. Version documents
	-- remain intact and will sync if Bob ever regains edit privileges. There is no data loss.

	rxTest:setTimeLimit( 60 )

	local assetDocId = '44ab164cd874418ca566d5a0e259ac0d'
	local newRevId1 =  'rrrr164cd874418ca566d5a0e259ac0d'
	
	local versionDocId = 'verb164cd874418ca566d5a0e259ac0d'
	local versionRevId = 'verrev4cd874418ca566d5a0e259ac0d'
	
	local expectedFormFactor
	
	if WFPlatformInfo.isMobile() then
		expectedFormFactor = 'phone'
	else
		expectedFormFactor = 'desktop'
	end

	local expectedResponses = {
		{
			url = string.format( "/v2/spaces/%s/updates", spaceId1 ),

			response = string.format( [[{
				"base": "https://photos.adobe.io",
				"updates": [ {
						"id": "%s",
						"created": "2015-05-27T00:34:56.669987Z",
						"updated": "2015-05-27T00:38:29.160369Z",
						"type": "asset",
						"subtype": "image",
						"revision_ids": [
							"a1e880f718a348b89663cf38f9f29d23",
							"cd1ab30cf8464ef2a9e736b50c0ba33d",
							"96f33a7215e04c9aa49346bbf8aae921",
							"813fdbda1b244c2d9f41008ba63eedba"
						],
						"links": {
							"self": {
								"href": "/v2/spaces/%s/assets/%s"
							}
						},
						"payload": {
							"importSource": {
								"importTimestamp": "2015-05-26T17:34:23-07:00",
								"originalHeight": 1600,
								"originalWidth": 1068
							},
							"captureDate": "2015-05-17T15:50:12",
							"xmp": {
								"dc": {
									"rights": "test copyright string"
								}
							},
							"location": {
								"longitude": -93.600107,
								"latitude": 45.951733,
								"altitude": 377.9
							}
						}
					}
				],
				"links": {
					"next": {
						"href": "/v2/spaces/%s/updates?updated_since=pull1"
					}
				}
			}]], assetDocId, spaceId1, assetDocId, spaceId1 ),

			headers = {
				status_code = '200',
			},

			options = {
				bypassRateLimiter = true,
			}

		},
		{
			url = string.format( "/v2/spaces/%s/updates?updated_since=pull1", spaceId1 ),

			response = string.format( [[{
				"base": "https://photos.adobe.io",
				"updates": [],
				"links": {
					"next": {
						"href": "/v2/spaces/%s/updates?updated_since=pull1"
					}
				}
			}]], spaceId1 ),

			headers = {
				status_code = '200',
			},

			options = {
				bypassRateLimiter = true,
			}

		},
		{
			url = string.format( "/v2/spaces/updates" ),
				
			data = string.format( [[[{"space_id":"%s","type":"asset","action":"update","revision_id":"%s","id":"%s","payload":{"importSource":{},"develop":{"xmpCameraRaw":"<xmp>HelloJeff</xmp>"}},"if_match":"813fdbda1b244c2d9f41008ba63eedba","generate_renditions":"all"},{"type":"version","action":"create","subtype":"edit","id":"%s","asset":{"id":"%s",},"space_id":"%s","payload":{"name":"foo","develop":{"xmpCameraRaw":"<xmp>HelloJeff</xmp>"}},"importedOnDevice":{"formFactor":"%s"},"revision_id":"%s"}] ]], 
					spaceId1, newRevId1, assetDocId, versionDocId, assetDocId, spaceId1, expectedFormFactor, versionRevId ),
			
			dataFilter = function( str )
			
				str = str:gsub( '"sha256":"[0-9a-f]+"', "" ) .. ' '

				return str
				
			end,
		
			options = {
				requestHeaders = {
					{
						header = "Content-Type",
						value = "application/json",
					},
					{
						header = "Accept",
						value = "application/json",
					},
				},
				requestMethod = "POST",
				bypassRateLimiter = true,
			},

		},
		{
			url = "/v2/spaces/updates?updated_since=pull1",
			
			response = [[{
			  "base": "https://photos-stage.adobe.io/v2/",
			  "updates": [
				{
				  "created": "2019-08-21T17:01:02.467393Z",
				  "type": "space",
				  "subtype": "shared",
				  "id": "<space1_spaceId>",
				  "owner": {
					"id": "<space1_spaceOwnerId>",
					"outOfQuota": false,
					"entitlementExpired": false,
					"links": {
					  "self": {
						"href": "spaces/<space1_spaceId>/updates/<space1_spaceOwnerId>"
					  }
					}
				  },
				  "this_space_user": {
					"id": "<space1_youSpaceUserId>",
					"user": {
					  "id": "<space1_youFeedbackUserId>",
					  "links": {
						"self": {
						  "href": "spaces/<space1_spaceId>/updates/<space1_youFeedbackUserId>"
						}
					  }
					},
					"links": {
					  "self": {
						"href": "spaces/<space1_spaceId>/updates/<space1_youSpaceUserId>"
					  }
					}
				  },
				  "albums": [
					{
					  "id": "<album1_id>",
					  "links": {
						"self": {
						  "href": "spaces/<space1_spaceId>/updates/<album1_id>"
						}
					  },
					  "space_album": {
						"id": "<spaceAlbum1_id>",
						"links": {
						  "self": {
							"href": "spaces/<space1_spaceId>/updates/<spaceAlbum1_id>"
						  }
						}
					  }
					}
				  ],
				  "revision_ids": [
					"33ca44a7da51f56e6d3f9e80b9181aaf",
					"813254e958ea3ffac99d32cda7acc969",
					"b6088deb146acbbde9268c50156fa19a",
					"a0cb906593fa65713d198cec5407426a",
					"3a7c6f13acb6fd7afa9d979681ae1e20",
					"8f8b861e17b67454062ab16b7437b189",
					"2ddfea16ac1a4af1cd0b9a1937fc9c3a",
					"d86b4baa17e22e23bf1a87e84e7d9807",
					"356b4baa17e22e23bf1a87e84e7d9807"
				  ],
				  "updated": "2019-08-22T17:01:08.471614Z",
				  "country_blocked": false,
				  "payload": {
					"userUpdated": "2019-08-21T17:01:08.445000Z",
					"allowInviteRequests": true,
					"userCreated": "2019-08-21T17:01:02.439000Z",
					"presentationFilters": true,
					"location": true,
					"download": true,
					"private": true,
					"metadata": true
				  },
				  "createdOnClient": "Chrome-76.0.3809-MacOSX",
				  "links": {
					"self": {
					  "href": "spaces/<space1_spaceId>/updates/<space1_spaceId>"
					},
					"current": {
					  "href": "spaces/<space1_spaceId>/updates?updated_since=pull2"
					},
					"/rels/web_space": {
					  "href": "http://bit.ly/2ZkCLDo"
					},
					"start": {
					  "href": "spaces/<space1_spaceId>/updates"
					}
				  }
				}
			  ],
			  "links": {
				"next": {
				  "href": "/v2/spaces/updates?updated_since=pull2"
				}
			  }
			}]],
			
			responseFilter = function( response )

				response = response:gsub( '<space1_spaceId>', space1.id )
				response = response:gsub( '<space1_youSpaceUserId>', space1.youSpaceUserId )
				response = response:gsub( '<space1_spaceOwnerId>', space1.ownerId or KSIdGenerator.uuidInLowercaseHex() )
				response = response:gsub( '<space1_youFeedbackUserId>', space1.youFeedbackUserId or KSIdGenerator.uuidInLowercaseHex() ) 
				
				response = response:gsub( '<album1_id>', space1.albumId )
				response = response:gsub( '<spaceAlbum1_id>', space1.spaceAlbumId )

				return response

			end,
			
			headers = {
				status_code = '200',
			},
			
			options = {
				bypassRateLimiter = true,
			}
				
		},
		{
			url = string.format( "/v2/spaces/%s/updates?updated_since=pull1", spaceId1 ),
			
			response = string.format( [[{
				"base": "https://photos.adobe.io",
				"updates": [ {
					  "updated": "2019-09-26T21:20:54.529292Z",
					  "type": "error",
					  "source": {
						"id": "%s",
						"type": "asset",
						"action": "update",
						"revision_id": "%s"
					  },
					  "subtype": "InsufficientPermissionError",
					  "id": "08eab133e5ba2161393aed62bee916d7",
					  "errors": {},
					  "status": 403
					},
					{
					  "updated": "2019-09-26T21:20:54.529292Z",
					  "type": "error",
					  "source": {
						"id": "%s",
						"type": "version",
						"action": "create",
						"revision_id": "%s"
					  },
					  "subtype": "InsufficientPermissionError",
					  "id": "z8eab133e5ba2161393aed62bee916d7",
					  "errors": {},
					  "status": 403
					}
				],
				"links": {
					"next": {
						"href": "/v2/spaces/%s/updates?updated_since=pull2"
					}
				}
			}]], assetDocId, newRevId1, versionDocId, versionRevId, spaceId1 ),

			responseFilter = function( response )

				response = response:gsub( '<space1_spaceId>', space1.id )
				response = response:gsub( '<space1_youSpaceUserId>', space1.youSpaceUserId )

				return response

			end,

			headers = {
				status_code = '200',
			},
			
			options = {
				bypassRateLimiter = true,
			}
			
		},
		{
			url = string.format( "/v2/spaces/%s/updates?updated_since=pull2", spaceId1 ),

			response = string.format( [[{
				"base": "https://photos.adobe.io",
				"updates": [],
				"links": {
					"next": {
						"href": "/v2/spaces/%s/updates?updated_since=pull2"
					}
				}
			}]], spaceId1 ),

			headers = {
				status_code = '200',
			},

			options = {
				bypassRateLimiter = true,
			}
		},
		{
			url = string.format( "/v2/spaces/%s/assets/%s", spaceId1, assetDocId ),

			response = string.format( [[
				{
					"id": "%s",
					"created": "2015-05-27T00:34:56.669987Z",
					"updated": "2015-05-27T00:38:29.160369Z",
					"type": "asset",
					"subtype": "image",
					"revision_ids": [
						"a1e880f718a348b89663cf38f9f29d23",
						"cd1ab30cf8464ef2a9e736b50c0ba33d",
						"96f33a7215e04c9aa49346bbf8aae921",
						"813fdbda1b244c2d9f41008ba63eedba"
					],
					"links": {
						"self": {
							"href": "/v2/spaces/%s/assets/%s"
						}
					},
					"payload": {
						"importSource": {
							"importTimestamp": "2015-05-26T17:34:23-07:00",
							"originalHeight": 1600,
							"originalWidth": 1068
						},
						"captureDate": "2015-05-17T15:50:12",
						"xmp": {
							"dc": {
								"rights": "test copyright string"
							}
						},
						"location": {
							"longitude": -93.600107,
							"latitude": 45.951733,
							"altitude": 377.9
						}
					}
				}
			]], assetDocId, spaceId1, assetDocId ),

			headers = {
				status_code = '200',
			},

			options = {
				bypassRateLimiter = true,
			}
		},
		{
			url = "/v2/spaces/updates?updated_since=pull2",
			
			response = [[{
			  "base": "https://photos-stage.adobe.io/v2/",
			  "updates": [
				{
				  "created": "2019-08-21T17:01:02.467393Z",
				  "type": "space",
				  "subtype": "shared",
				  "id": "<space1_spaceId>",
				  "owner": {
					"id": "<space1_spaceOwnerId>",
					"outOfQuota": false,
					"entitlementExpired": false,
					"links": {
					  "self": {
						"href": "spaces/<space1_spaceId>/updates/<space1_spaceOwnerId>"
					  }
					}
				  },
				  "this_space_user": {
					"id": "<space1_youSpaceUserId>",
					"user": {
					  "id": "<space1_youFeedbackUserId>",
					  "links": {
						"self": {
						  "href": "spaces/<space1_spaceId>/updates/<space1_youFeedbackUserId>"
						}
					  }
					},
					"links": {
					  "self": {
						"href": "spaces/<space1_spaceId>/updates/<space1_youSpaceUserId>"
					  }
					}
				  },
				  "albums": [
					{
					  "id": "<album1_id>",
					  "links": {
						"self": {
						  "href": "spaces/<space1_spaceId>/updates/<album1_id>"
						}
					  },
					  "space_album": {
						"id": "<spaceAlbum1_id>",
						"links": {
						  "self": {
							"href": "spaces/<space1_spaceId>/updates/<spaceAlbum1_id>"
						  }
						}
					  }
					}
				  ],
				  "revision_ids": [
					"33ca44a7da51f56e6d3f9e80b9181aaf",
					"813254e958ea3ffac99d32cda7acc969",
					"b6088deb146acbbde9268c50156fa19a",
					"a0cb906593fa65713d198cec5407426a",
					"3a7c6f13acb6fd7afa9d979681ae1e20",
					"8f8b861e17b67454062ab16b7437b189",
					"2ddfea16ac1a4af1cd0b9a1937fc9c3a",
					"d86b4baa17e22e23bf1a87e84e7d9807"
				  ],
				  "updated": "2019-08-21T17:01:08.471614Z",
				  "country_blocked": false,
				  "payload": {
					"userUpdated": "2019-08-21T17:01:08.445000Z",
					"allowInviteRequests": true,
					"userCreated": "2019-08-21T17:01:02.439000Z",
					"presentationFilters": true,
					"location": true,
					"download": true,
					"private": true,
					"metadata": true
				  },
				  "createdOnClient": "Chrome-76.0.3809-MacOSX",
				  "links": {
					"self": {
					  "href": "spaces/<space1_spaceId>/updates/<space1_spaceId>"
					},
					"current": {
					  "href": "spaces/<space1_spaceId>/updates?updated_since=pull3"
					},
					"/rels/web_space": {
					  "href": "http://bit.ly/2ZkCLDo"
					},
					"start": {
					  "href": "spaces/<space1_spaceId>/updates"
					}
				  }
				}
			  ],
			  "links": {
				"next": {
				  "href": "/v2/spaces/updates?updated_since=pull2"
				}
			  }
			}]],
			
			responseFilter = function( response )

				response = response:gsub( '<space1_spaceId>', space1.id )
				response = response:gsub( '<space1_youSpaceUserId>', space1.youSpaceUserId )
				response = response:gsub( '<space1_spaceOwnerId>', space1.ownerId or KSIdGenerator.uuidInLowercaseHex() )
				response = response:gsub( '<space1_youFeedbackUserId>', space1.youFeedbackUserId or KSIdGenerator.uuidInLowercaseHex() ) 
				
				response = response:gsub( '<album1_id>', space1.albumId )
				response = response:gsub( '<spaceAlbum1_id>', space1.spaceAlbumId )

				return response

			end,
			
			headers = {
				status_code = '200',
			},
			
			options = {
				bypassRateLimiter = true,
			}
				
		},
		{
			url = string.format( "/v2/spaces/%s/updates?updated_since=pull2", spaceId1 ),
			
			response = string.format( [[{
				"base": "https://photos.adobe.io",
				"updates": [ {
					"updated": "2019-07-25T22:27:41.466149Z",
					  "type": "space_user",
					  "payload": {
						"status": "accepted",
						"role": "editor",
						"userUpdated": "2019-07-25T22:27:41.299863Z",
						"userCreated": "2019-07-25T22:27:41.299863Z"
					  },
					  "id": "<space1_youSpaceUserId>",
					  "user": {
						"id": "87bd59dd4c01e4f71155d7bfb50ccdf3"
					  },
					  "created": "2019-07-25T22:27:41.466149Z",
					  "revision_ids": [
						"ae43e159e80148fb47a8fe3ec4e6a718"
					  ]
					}
				],
				"links": {
					"next": {
						"href": "/v2/spaces/%s/updates?updated_since=pull3"
					}
				}
			}]], spaceId1 ),
			
			responseFilter = function( response )

				response = response:gsub( '<space1_spaceId>', space1.id )
				response = response:gsub( '<space1_youSpaceUserId>', space1.youSpaceUserId )

				return response

			end,

			headers = {
				status_code = '200',
			},
			
			options = {
				bypassRateLimiter = true,
			}
			
		},
		{
			url = string.format( "/v2/spaces/%s/updates?updated_since=pull3", spaceId1 ),

			response = string.format( [[{
				"base": "https://photos.adobe.io",
				"updates": [],
				"links": {
					"next": {
						"href": "/v2/spaces/%s/updates?updated_since=pull3"
					}
				}
			}]], spaceId1 ),

			headers = {
				status_code = '200',
			},

			options = {
				bypassRateLimiter = true,
			}

		},
		{
			url = string.format( "/v2/spaces/updates" ),
				
			data = string.format( [[[{"type":"version","action":"create","subtype":"edit","id":"%s","asset":{"id":"%s",},"space_id":"%s","payload":{"name":"foo","develop":{"xmpCameraRaw":"<xmp>HelloJeff</xmp>"}},"importedOnDevice":{"formFactor":"%s"},"revision_id":"%s"}] ]], 
					versionDocId, assetDocId, spaceId1, expectedFormFactor, versionRevId ),
					
			dataFilter = function( str )
			
				str = str:gsub( '"sha256":"[0-9a-f]+"', "" ) .. ' '

				return str
				
			end,
		
			options = {
				requestHeaders = {
					{
						header = "Content-Type",
						value = "application/json",
					},
					{
						header = "Accept",
						value = "application/json",
					},
				},
				requestMethod = "POST",
				bypassRateLimiter = true,
			},

		},
	}
	
	local args = {
		spaceUser1Role = 'contributor'
	}
	
	local catalogSpec = createCatalogSpecForTest( rxTest, space1, nil, expectedResponses, args )

	local wfCatalog = createWFCatalogForTestWithSpec( rxTest, catalogSpec )
	
	-- Setup the error handler
	
	local errorHandler = WFCatErrorHandler( wfCatalog, {
			deleteAssets = bulkDelete,
			deleteIndividualAlbum = deleteDoc
		},
		{ debugMode = true } )
	
	rxTest:addCloseTarget( errorHandler )
	
	-- Start up the space feed manager
	
	local spaceFeedsManagerArgs = {
		taskDelays = {
			progressiveBackoff = false,
			noWorkDelay = 0.5,
			maxNoWorkDelay = 1,
			errorDelay = 1,
			maxDelay = 1,
		},
		albumAssetsForFilteredAssetsQueryFn = albumAssetsForFilteredAssetsQueryFn,
		testMode = true
	}

	local spaceFeedsManager = WFCatSpaceFeedsManager( wfCatalog, spaceFeedsManagerArgs )
	rxTest:addCloseTarget( spaceFeedsManager )
	
	-- Verify the space is active.
	
	local activeSpacesObserver = rxTest:observe( spaceFeedsManager._activeSpacesStream )
	
	local isDownloadingObserver = rxTest:observe( spaceFeedsManager:isDownloadingMetadata() )
	
	rxTest:expectOnlyCompleted( spaceFeedsManager:_testOnly_getUpdatesOnce() )
	
	activeSpacesObserver:expectDataWithinTime( 2, spaceId1, spaceUserId1, ":ignore:", false, ":ignore:", ":ignore:" )

	isDownloadingObserver:expectDataWithinTime( 5, true ) -- Space 1
	isDownloadingObserver:expectDataWithinTime( 5, false )
	
	isDownloadingObserver:dispose()
	
	-- Now edit the asset and attempt to send the changes to Oz, even though we
	-- lack the proper priveleges. Also create a version w/ these same
	-- develop changes.

	local simpleXmp = '<xmp>HelloJeff</xmp>'

	local overlay = {
		_id = assetDocId,
		_newRevId = newRevId1,
		importSource = {
			sha256 = "5a7ed787c67503c2dcf8aba4a8e23d11e278bf00b0a740978d82815bf67853c2",
		},
		develop = {
			xmpCameraRaw = simpleXmp
		}
	}
	
	rxTest:waitFor( wfCatalog:docStore():applyOverlay( overlay ) )
	
	rxTest:expectSingleValue( wfCatalog:docStore():docValue( assetDocId )
		:pluck( 'develop', 'xmpCameraRaw' ), simpleXmp )
	
	rxTest:expectSingleValue( wfCatalog:docStore():docValue( assetDocId )
		:pluck( '_spaceId' ), spaceId1 )
	
	local versionDocValue = {
		_id = versionDocId,
		_newRevId = versionRevId,
		_spaceId = spaceId1,
		type = 'version',
		subtype = 'edit',
		name = 'foo',
		asset = {
			id = assetDocId,
			sha256 = KSIdGenerator.uuidInLowercaseHex()
		},
		importedOnDevice = {
			formFactor = WFPlatformInfo.isDesktop and "desktop" or 
				WFPlatformInfo.isPhone and "phone" or 
				WFPlatformInfo.isTablet and "tablet"
		},
		develop = {
			xmpCameraRaw = simpleXmp
		}
	}
	
	rxTest:waitFor( wfCatalog:docStore():setDocValue( versionDocValue ) )
	
	local sendNUpdates = rxTest:observe( spaceFeedsManager._spacePusher:_DEBUG_sendNUpdates( 2 ) )
	sendNUpdates:expectCompletedWithinTime( 5 )
	sendNUpdates:dispose()
	
	-- Get more updates. This should fetch the error docs created from attempting
	-- to push the asset update and version create.
	
	rxTest:expectOnlyCompleted( spaceFeedsManager:_testOnly_getUpdatesOnce() )
	
	-- Verify the version doc is marked with the insufficient permission error.
	
	local errorObserver = rxTest:observe( wfCatalog:docStore():ongoingDocValue( versionDocId )
			:pluck( '_localOnly', 'ozError', 'subtype' ) )
	errorObserver:expectDataWithinTime( 10, 'InsufficientPermissionError' )
	
	-- Verify that the asset document was reset to the Oz revision by the error handler.
	
	local monitorXmpCameraRaw = rxTest:observe( wfCatalog:docStore():ongoingDocValue( assetDocId )
			:pluck( 'develop', 'xmpCameraRaw' ) )
	monitorXmpCameraRaw:expectDataWithinTime( 10, nil )
	monitorXmpCameraRaw:dispose()
	
	-- Get more updates. This should update spaceUser1 to an editor. As a result
	-- the version documents associated with the discarded revision should now
	-- sync to Oz.
	
	rxTest:expectOnlyCompleted( spaceFeedsManager:_testOnly_getUpdatesOnce() )
	
	activeSpacesObserver:expectDataWithinTime( 10, spaceId1, spaceUserId1, ":ignore:", false, ":ignore:", ":ignore:" )
	
	activeSpacesObserver:dispose()
	
	-- Verify the version doc is no longer marked with InsufficientPermissionError
			
	errorObserver:expectDataWithinTime( 10, nil )
	
	errorObserver:dispose()
	
	-- Push the Version to Oz.
	
	sendNUpdates = rxTest:observe( spaceFeedsManager._spacePusher:_DEBUG_sendUpdatesOnce() )
	sendNUpdates:expectCompletedWithinTime( 10 )
	sendNUpdates:dispose()
	
end

-----------------------------------------------------------------------

function WFCatSpaceFeedsManagerTest.rxTestDeprecatedDocMigration( rxTest )

	rxTest:setTimeLimit( 60 )
	
	local expectedResponses = { 
		{
			url = string.format( "/v2/spaces/%s/updates?updated_since=pull2", spaceId1 ),

			response = string.format( [[{
				"base": "https://photos.adobe.io",
				"updates": [],
				"links": {
					"next": {
						"href": "/v2/spaces/%s/updates?updated_since=pull2"
					}
				}
			}]], spaceId1 ),

			headers = {
				status_code = '200',
			},

			options = {
				bypassRateLimiter = true,
			}

		}
 
 	}
	
	local args = {
		spacesAreOverQuota = false,
		entitlementExpired = false,
	}
	
	local catalogSpec = createCatalogSpecForTest( rxTest, space1, nil, expectedResponses, args )
	
	local wfCatalog = createWFCatalogForTestWithSpec( rxTest, catalogSpec )
	
	-- Create a deprecated local doc that tracks the next feed link.
	
	local deprecatedIdentifier = string.format( "Space %s Puller", spaceId1 )
			
	local deprecatedNextFeedLocalDocId = 
			WFCatMetadataPuller.getNextUpdateFeedLinkLocalDocId( false, deprecatedIdentifier )
	
	rxTest:expectOnlyCompleted( wfCatalog:docStore():setLocalDocValue( {
					_id = deprecatedNextFeedLocalDocId,
					link = string.format( "/v2/spaces/%s/updates?updated_since=pull2", spaceId1 )
				} ) )

	-- Start the space feeds manager. This should migrate the deprecated doc to the _new_ doc.
	
	local spaceFeedsManagerArgs = {
		taskDelays = {
			progressiveBackoff = false,
			noWorkDelay = 0.5,
			maxNoWorkDelay = 1,
			errorDelay = 1,
			maxDelay = 1,
		},
		isPrefetchEnabled = true,
		albumAssetsForFilteredAssetsQueryFn = albumAssetsForFilteredAssetsQueryFn,
		testMode = true,
	}

	local spaceFeedsManager = WFCatSpaceFeedsManager( wfCatalog, spaceFeedsManagerArgs )
	rxTest:addCloseTarget( spaceFeedsManager )
	
	local observer = rxTest:observe( spaceFeedsManager:isDownloadingMetadata() )
	
	rxTest:expectOnlyCompleted( spaceFeedsManager:_testOnly_getUpdatesOnce() )
	
	observer:expectDataWithinTime( 10, true ) -- Space 1
	observer:expectDataWithinTime( 10, false )
	
	observer:dispose()
	
	local expectedLogData = {
		documentsReceivedByType = {},
		successfulUpdateCycles = 1,
		totalDocumentsReceived = 0,
	}

	local puller = spaceFeedsManager._spacePullers[ spaceUserId1 ]
	
	local expectedLink = string.format( '/v2/spaces/%s/updates?updated_since=pull2', spaceId1 )
	
	verifyLogData( rxTest, puller:getDiagnosticLogData(), expectedLogData, expectedLink, spaceUserId1 )
	 
end

--------------------------------------------------------------------------------

function WFCatSpaceFeedsManagerTest.rxTestAbandonedLocalSpace( rxTest )

	rxTest:setTimeLimit( 30 )

	local expectedResponses = {
		{
			url = string.format( "/v2/spaces/%s/updates", spaceId1 ),

			response = string.format( [[{
				"base": "https://photos.adobe.io",
				"updates": [ {
						"id": "8462f5ba3b39fd050d3cd6b7352ba561",
						"created": "2015-05-27T00:34:56.669987Z",
						"updated": "2015-05-27T00:38:29.160369Z",
						"type": "asset",
						"subtype": "image",
						"revision_ids": [
							"a1e880f718a348b89663cf38f9f29d23",
							"cd1ab30cf8464ef2a9e736b50c0ba33d",
							"96f33a7215e04c9aa49346bbf8aae921",
							"813fdbda1b244c2d9f41008ba63eedba"
						],
						"payload": {
							"importSource": {
								"importTimestamp": "2015-05-26T17:34:23-07:00",
								"originalHeight": 1600,
								"originalWidth": 1068
							},
							"captureDate": "2015-05-17T15:50:12",
							"xmp": {
								"dc": {
									"rights": "test copyright string"
								}
							},
							"location": {
								"longitude": -93.600107,
								"latitude": 45.951733,
								"altitude": 377.9
							}
						}
					}
				],
				"links": {
					"next": {
						"href": "/v2/spaces/%s/updates?updated_since=pull1"
					}
				}
			}]], spaceId1 ),

			headers = {
				status_code = '200',
			},

			options = {
				bypassRateLimiter = true,
			}

		},
		{
			url = string.format( "/v2/spaces/%s/updates?updated_since=pull1", spaceId1 ),

			response = string.format( [[{
				"base": "https://photos.adobe.io",
				"updates": [],
				"links": {
					"next": {
						"href": "/v2/spaces/%s/updates?updated_since=pull1"
					}
				}
			}]], spaceId1 ),

			headers = {
				status_code = '200',
			},

			options = {
				bypassRateLimiter = true,
			}

		}
	}
	
	local args = {
		spacesAreOverQuota = false,
		entitlementExpired = false,
	}
	
	local catalogSpec = createCatalogSpecForTest( rxTest, space1, nil, expectedResponses, args )
	
	local wfCatalog = createWFCatalogForTestWithSpec( rxTest, catalogSpec )
	
	local spaceFeedsManagerArgs = {
		taskDelays = {
			progressiveBackoff = false,
			noWorkDelay = 0.5,
			maxNoWorkDelay = 1,
			errorDelay = 1,
			maxDelay = 1,
		},
		isPrefetchEnabled = true,
		albumAssetsForFilteredAssetsQueryFn = albumAssetsForFilteredAssetsQueryFn,
		testMode = true,
	}

	-- Start up the space feed manager

	local spaceFeedsManager = WFCatSpaceFeedsManager( wfCatalog, spaceFeedsManagerArgs )
	rxTest:addCloseTarget( spaceFeedsManager )
	
	-- Verify the puller is active.
	
	local observer = rxTest:observe( spaceFeedsManager:isDownloadingMetadata() )
	
	rxTest:expectOnlyCompleted( spaceFeedsManager:_testOnly_getUpdatesOnce() )
	
	observer:expectDataWithinTime( 2, true ) -- Space 1
	observer:expectDataWithinTime( 2, false )
	
	observer:dispose()
	
	local puller = spaceFeedsManager._spacePullers[ spaceUserId1 ]
	
	rxTest:expectNotEqual( nil, puller )
	
	-- Abandon the space locally.
	
	local overlay = WFCatUtils.createBaseOverlayWithUserUpdatedField( spaceUserId1 )
	overlay.status = 'abandoned'
	
	rxTest:expectSingleValue( wfCatalog:docStore():applyOverlay( overlay )
		:map( function()
			return true
		end ), true )
	
	-- Verify the puller is shut down.
	
	puller = spaceFeedsManager._spacePullers[ spaceUserId1 ]
	
	rxTest:expectEqual( nil, puller )

end

--------------------------------------------------------------------------------

function WFCatSpaceFeedsManagerTest.rxTestSpaceDeletedOnOz( rxTest )

	rxTest:setTimeLimit( 30 )

	local expectedResponses = {
		{
			url = string.format( "/v2/spaces/%s/updates", spaceId1 ),

			response = string.format( [[{
				"base": "https://photos.adobe.io",
				"updates": [ {
						"id": "8462f5ba3b39fd050d3cd6b7352ba561",
						"created": "2015-05-27T00:34:56.669987Z",
						"updated": "2015-05-27T00:38:29.160369Z",
						"type": "asset",
						"subtype": "image",
						"revision_ids": [
							"a1e880f718a348b89663cf38f9f29d23",
							"cd1ab30cf8464ef2a9e736b50c0ba33d",
							"96f33a7215e04c9aa49346bbf8aae921",
							"813fdbda1b244c2d9f41008ba63eedba"
						],
						"payload": {
							"importSource": {
								"importTimestamp": "2015-05-26T17:34:23-07:00",
								"originalHeight": 1600,
								"originalWidth": 1068
							},
							"captureDate": "2015-05-17T15:50:12",
							"xmp": {
								"dc": {
									"rights": "test copyright string"
								}
							},
							"location": {
								"longitude": -93.600107,
								"latitude": 45.951733,
								"altitude": 377.9
							}
						}
					}
				],
				"links": {
					"next": {
						"href": "/v2/spaces/%s/updates?updated_since=pull1"
					}
				}
			}]], spaceId1 ),

			headers = {
				status_code = '200',
			},

			options = {
				bypassRateLimiter = true,
			}

		},
		{
			url = string.format( "/v2/spaces/%s/updates?updated_since=pull1", spaceId1 ),

			response = string.format( [[{
				"base": "https://photos.adobe.io",
				"updates": [],
				"links": {
					"next": {
						"href": "/v2/spaces/%s/updates?updated_since=pull1"
					}
				}
			}]], spaceId1 ),

			headers = {
				status_code = '200',
			},

			options = {
				bypassRateLimiter = true,
			}

		},
		{
		
			url = "/v2/spaces/updates?updated_since=pull1",
			
			response = [[{
			  "base": "https://photos-stage.adobe.io/v2/",
			  "updates": [
				{
				  "created": "2019-08-21T17:01:02.467393Z",
				  "type": "space",
				  "subtype": "shared",
				  "id": "<space1_spaceId>",
				  "owner": {
					"id": "<space1_spaceOwnerId>",
					"outOfQuota": false,
					"entitlementExpired": false,
					"links": {
					  "self": {
						"href": "spaces/<space1_spaceId>/updates/<space1_spaceOwnerId>"
					  }
					}
				  },
				  "this_space_user": {
					"id": "<space1_youSpaceUserId>",
					"user": {
					  "id": "<space1_youFeedbackUserId>",
					  "links": {
						"self": {
						  "href": "spaces/<space1_spaceId>/updates/<space1_youFeedbackUserId>"
						}
					  }
					},
					"links": {
					  "self": {
						"href": "spaces/<space1_spaceId>/updates/<space1_youSpaceUserId>"
					  }
					}
				  },
				  "albums": [
					{
					  "id": "<album1_id>",
					  "links": {
						"self": {
						  "href": "spaces/<space1_spaceId>/updates/<album1_id>"
						}
					  },
					  "space_album": {
						"id": "<spaceAlbum1_id>",
						"links": {
						  "self": {
							"href": "spaces/<space1_spaceId>/updates/<spaceAlbum1_id>"
						  }
						}
					  }
					}
				  ],
				  "revision_ids": [
					"33ca44a7da51f56e6d3f9e80b9181aaf",
					"813254e958ea3ffac99d32cda7acc969",
					"b6088deb146acbbde9268c50156fa19a",
					"a0cb906593fa65713d198cec5407426a",
					"3a7c6f13acb6fd7afa9d979681ae1e20",
					"8f8b861e17b67454062ab16b7437b189",
					"2ddfea16ac1a4af1cd0b9a1937fc9c3a",
					"d86b4baa17e22e23bf1a87e84e7d9807"
				  ],
				  "updated": "2019-08-21T17:01:08.471614Z",
				  "country_blocked": false,
				  "payload": {
					"userUpdated": "2019-08-21T17:01:08.445000Z",
					"allowInviteRequests": true,
					"userCreated": "2019-08-21T17:01:02.439000Z",
					"presentationFilters": true,
					"location": true,
					"download": true,
					"private": true,
					"metadata": true
				  },
				  "createdOnClient": "Chrome-76.0.3809-MacOSX",
				  "links": {
					"self": {
					  "href": "spaces/<space1_spaceId>/updates/<space1_spaceId>"
					},
					"current": {
					  "href": "spaces/<space1_spaceId>/updates?updated_since=pull2"
					},
					"/rels/web_space": {
					  "href": "http://bit.ly/2ZkCLDo"
					},
					"start": {
					  "href": "spaces/<space1_spaceId>/updates"
					}
				  }
				}
			  ],
			  "links": {
				"next": {
				  "href": "/v2/spaces/updates?updated_since=pull1"
				}
			  }
			}]],
			
			responseFilter = function( response )

				response = response:gsub( '<space1_spaceId>', space1.id )
				response = response:gsub( '<space1_youSpaceUserId>', space1.youSpaceUserId )
				response = response:gsub( '<space1_spaceOwnerId>', space1.ownerId or KSIdGenerator.uuidInLowercaseHex() )
				response = response:gsub( '<space1_youFeedbackUserId>', space1.youFeedbackUserId or KSIdGenerator.uuidInLowercaseHex() ) 
				
				response = response:gsub( '<album1_id>', space1.albumId )
				response = response:gsub( '<spaceAlbum1_id>', space1.spaceAlbumId )

				return response

			end,
			
			headers = {
				status_code = '200',
			},
			
			options = {
				bypassRateLimiter = true,
			}
				
		},
		{
			url = string.format( "/v2/spaces/%s/updates?updated_since=pull1", spaceId1 ),

			fail = {
				[[ {
					"errors": {
						"space": [
							"does not exist"
						]
					}
				} ]],
				{
					status_code = '404',
				}
			},

			options = {
				bypassRateLimiter = true,
			}

		},
	}
	
	local args = {
		spacesAreOverQuota = false,
		entitlementExpired = false,
	}
	
	local catalogSpec = createCatalogSpecForTest( rxTest, space1, nil, expectedResponses, args )
	
	local wfCatalog = createWFCatalogForTestWithSpec( rxTest, catalogSpec )
	
	local spaceFeedsManagerArgs = {
		taskDelays = {
			progressiveBackoff = false,
			noWorkDelay = 0.5,
			maxNoWorkDelay = 1,
			errorDelay = 1,
			maxDelay = 1,
		},
		isPrefetchEnabled = true,
		albumAssetsForFilteredAssetsQueryFn = albumAssetsForFilteredAssetsQueryFn,
		testMode = true,
	}

	-- Start up the space feed manager

	local spaceFeedsManager = WFCatSpaceFeedsManager( wfCatalog, spaceFeedsManagerArgs )
	rxTest:addCloseTarget( spaceFeedsManager )
	
	-- Verify the puller is active.
	
	local observer = rxTest:observe( spaceFeedsManager:isDownloadingMetadata() )
	
	rxTest:expectOnlyCompleted( spaceFeedsManager:_testOnly_getUpdatesOnce() )
	
	observer:expectDataWithinTime( 2, true ) -- Space 1
	observer:expectDataWithinTime( 2, false )
	
	observer:dispose()
	
	local puller = spaceFeedsManager._spacePullers[ spaceUserId1 ]
	
	rxTest:expectNotEqual( nil, puller )
	
	-- Get more updates. This should fetch the space deleted error on the feed of feeds.
	
	local inactiveSpaceStream = rxTest:observe( spaceFeedsManager._inactiveSpacesStream )
	
	rxTest:expectOnlyCompleted( spaceFeedsManager:_testOnly_getUpdatesOnce() )
	
	inactiveSpaceStream:expectDataWithinTime( 5, spaceUserId1, false )
	
	inactiveSpaceStream:dispose()
	
	-- Verify the puller is shut down.
	
	puller = spaceFeedsManager._spacePullers[ spaceUserId1 ]
	
	rxTest:expectEqual( nil, puller )
	
end

--------------------------------------------------------------------------------

function WFCatSpaceFeedsManagerTest.rxTestOwnerExpirationWithUpdateJustPushed_testNightly( rxTest )

	rxTest:setTimeLimit( 30 )
	
	local assetDocId = '44ab164cd874418ca566d5a0e259ac0d'
	local newRevId1 =  'rrrr164cd874418ca566d5a0e259ac0d'
	local captureDate = '2019-09-26T18:13:30.050Z'

	local expectedResponses = {
		{
			url = string.format( "/v2/spaces/%s/updates", spaceId1 ),

			response = string.format( [[{
				"base": "https://photos.adobe.io",
				"updates": [ {
						"id": "8462f5ba3b39fd050d3cd6b7352ba561",
						"created": "2015-05-27T00:34:56.669987Z",
						"updated": "2015-05-27T00:38:29.160369Z",
						"type": "asset",
						"subtype": "image",
						"revision_ids": [
							"a1e880f718a348b89663cf38f9f29d23",
							"cd1ab30cf8464ef2a9e736b50c0ba33d",
							"96f33a7215e04c9aa49346bbf8aae921",
							"813fdbda1b244c2d9f41008ba63eedba"
						],
						"payload": {
							"importSource": {
								"importTimestamp": "2015-05-26T17:34:23-07:00",
								"originalHeight": 1600,
								"originalWidth": 1068
							},
							"captureDate": "2015-05-17T15:50:12",
							"xmp": {
								"dc": {
									"rights": "test copyright string"
								}
							},
							"location": {
								"longitude": -93.600107,
								"latitude": 45.951733,
								"altitude": 377.9
							}
						}
					}
				],
				"links": {
					"next": {
						"href": "/v2/spaces/%s/updates?updated_since=pull1"
					}
				}
			}]], spaceId1 ),

			headers = {
				status_code = '200',
			},

			options = {
				bypassRateLimiter = true,
			}

		},
		{
			url = string.format( "/v2/spaces/%s/updates?updated_since=pull1", spaceId1 ),

			response = string.format( [[{
				"base": "https://photos.adobe.io",
				"updates": [],
				"links": {
					"next": {
						"href": "/v2/spaces/%s/updates?updated_since=pull1"
					}
				}
			}]], spaceId1 ),

			headers = {
				status_code = '200',
			},

			options = {
				bypassRateLimiter = true,
			}

		},
		{
			url = string.format( "/v2/spaces/updates" ),
				
			data = string.format( [[[{"space_id":"%s","type":"asset","action":"create","revision_id":"%s","id":"%s","payload":{"importSource":{},"captureDate":"%s"},"subtype":"image"}] ]], 
					spaceId1, newRevId1, assetDocId, captureDate ),
			
			dataFilter = function( str )
			
				str = str:gsub( '"sha256":"[0-9a-f]+"', "" ) .. ' '

				return str
				
			end,
		
			options = {
				requestHeaders = {
					{
						header = "Content-Type",
						value = "application/json",
					},
					{
						header = "Accept",
						value = "application/json",
					},
				},
				requestMethod = "POST",
				bypassRateLimiter = true,
			},

		},
		{
			url = "/v2/spaces/updates?updated_since=pull1",
			
			response = [[{
			  "base": "https://photos-stage.adobe.io/v2/",
			  "updates": [
				{
				  "created": "2019-08-21T17:01:02.467393Z",
				  "type": "space",
				  "subtype": "shared",
				  "id": "<space1_spaceId>",
				  "owner": {
					"id": "<space1_spaceOwnerId>",
					"outOfQuota": false,
					"entitlementExpired": false,
					"links": {
					  "self": {
						"href": "spaces/<space1_spaceId>/updates/<space1_spaceOwnerId>"
					  }
					}
				  },
				  "this_space_user": {
					"id": "<space1_youSpaceUserId>",
					"user": {
					  "id": "<space1_youFeedbackUserId>",
					  "links": {
						"self": {
						  "href": "spaces/<space1_spaceId>/updates/<space1_youFeedbackUserId>"
						}
					  }
					},
					"links": {
					  "self": {
						"href": "spaces/<space1_spaceId>/updates/<space1_youSpaceUserId>"
					  }
					}
				  },
				  "albums": [
					{
					  "id": "<album1_id>",
					  "links": {
						"self": {
						  "href": "spaces/<space1_spaceId>/updates/<album1_id>"
						}
					  },
					  "space_album": {
						"id": "<spaceAlbum1_id>",
						"links": {
						  "self": {
							"href": "spaces/<space1_spaceId>/updates/<spaceAlbum1_id>"
						  }
						}
					  }
					}
				  ],
				  "revision_ids": [
					"33ca44a7da51f56e6d3f9e80b9181aaf",
					"813254e958ea3ffac99d32cda7acc969",
					"b6088deb146acbbde9268c50156fa19a",
					"a0cb906593fa65713d198cec5407426a",
					"3a7c6f13acb6fd7afa9d979681ae1e20",
					"8f8b861e17b67454062ab16b7437b189",
					"2ddfea16ac1a4af1cd0b9a1937fc9c3a",
					"d86b4baa17e22e23bf1a87e84e7d9807",
					"356b4baa17e22e23bf1a87e84e7d9807"
				  ],
				  "updated": "2019-08-22T17:01:08.471614Z",
				  "country_blocked": false,
				  "payload": {
					"userUpdated": "2019-08-21T17:01:08.445000Z",
					"allowInviteRequests": true,
					"userCreated": "2019-08-21T17:01:02.439000Z",
					"presentationFilters": true,
					"location": true,
					"download": true,
					"private": true,
					"metadata": true
				  },
				  "createdOnClient": "Chrome-76.0.3809-MacOSX",
				  "links": {
					"self": {
					  "href": "spaces/<space1_spaceId>/updates/<space1_spaceId>"
					},
					"current": {
					  "href": "spaces/<space1_spaceId>/updates?updated_since=pull1"
					},
					"/rels/web_space": {
					  "href": "http://bit.ly/2ZkCLDo"
					},
					"start": {
					  "href": "spaces/<space1_spaceId>/updates"
					}
				  }
				}
			  ],
			  "links": {
				"next": {
				  "href": "/v2/spaces/updates?updated_since=pull2"
				}
			  }
			}]],
			
			responseFilter = function( response )

				response = response:gsub( '<space1_spaceId>', space1.id )
				response = response:gsub( '<space1_youSpaceUserId>', space1.youSpaceUserId )
				response = response:gsub( '<space1_spaceOwnerId>', space1.ownerId or KSIdGenerator.uuidInLowercaseHex() )
				response = response:gsub( '<space1_youFeedbackUserId>', space1.youFeedbackUserId or KSIdGenerator.uuidInLowercaseHex() ) 
				
				response = response:gsub( '<album1_id>', space1.albumId )
				response = response:gsub( '<spaceAlbum1_id>', space1.spaceAlbumId )
				
				response = response:gsub( '"entitlementExpired": false', '"entitlementExpired": true' ) 

				return response

			end,
			
			headers = {
				status_code = '200',
			},
			
			options = {
				bypassRateLimiter = true,
			}
				
		},
		{
			url = string.format( "/v2/spaces/%s/updates?updated_since=pull1", spaceId1 ),
			
			response = string.format( [[{
				"base": "https://photos.adobe.io",
				"updates": [ {
					  "updated": "2019-09-26T21:20:54.529292Z",
					  "type": "error",
					  "source": {
						"id": "%s",
						"type": "asset",
						"action": "create",
						"revision_id": "%s"
					  },
					  "subtype": "TrialOrSubscriptionExpiredError",
					  "id": "08eab133e5ba2161393aed62bee916d7",
					  "errors": {},
					  "status": 403
					}
				],
				"links": {
					"next": {
						"href": "/v2/spaces/%s/updates?updated_since=pull2"
					}
				}
			}]], assetDocId, newRevId1, spaceId1 ),

			responseFilter = function( response )

				response = response:gsub( '<space1_spaceId>', space1.id )
				response = response:gsub( '<space1_youSpaceUserId>', space1.youSpaceUserId )

				return response

			end,

			headers = {
				status_code = '200',
			},
			
			options = {
				bypassRateLimiter = true,
			}
			
		},
		{
			url = string.format( "/v2/spaces/%s/updates?updated_since=pull2", spaceId1 ),

			response = string.format( [[{
				"base": "https://photos.adobe.io",
				"updates": [],
				"links": {
					"next": {
						"href": "/v2/spaces/%s/updates?updated_since=pull2"
					}
				}
			}]], spaceId1 ),

			headers = {
				status_code = '200',
			},

			options = {
				bypassRateLimiter = true,
			}

		},
	}
	
	local args = {
		spacesAreOverQuota = false,
		entitlementExpired = false,
	}
	
	local catalogSpec = createCatalogSpecForTest( rxTest, space1, nil, expectedResponses, args )
	
	local wfCatalog = createWFCatalogForTestWithSpec( rxTest, catalogSpec )
	
	local errorHandler = WFCatErrorHandler( wfCatalog,{
		deleteAssets = bulkDelete,
		deleteIndividualAlbum = deleteDoc },
		{ debugMode = true } )
	
	rxTest:addCloseTarget( errorHandler )
	
	local spaceFeedsManagerArgs = {
		taskDelays = {
			progressiveBackoff = false,
			noWorkDelay = 0.5,
			maxNoWorkDelay = 1,
			errorDelay = 1,
			maxDelay = 1,
		},
		isPrefetchEnabled = true,
		albumAssetsForFilteredAssetsQueryFn = albumAssetsForFilteredAssetsQueryFn,
		testMode = true,
	}

	-- Start up the space feed manager

	local spaceFeedsManager = WFCatSpaceFeedsManager( wfCatalog, spaceFeedsManagerArgs )
	rxTest:addCloseTarget( spaceFeedsManager )
	
	-- Verify the puller is active.
	
	local observer = rxTest:observe( spaceFeedsManager:isDownloadingMetadata() )
	
	rxTest:expectOnlyCompleted( spaceFeedsManager:_testOnly_getUpdatesOnce() )
	
	observer:expectDataWithinTime( 2, true ) -- Space 1
	observer:expectDataWithinTime( 2, false )
	
	observer:dispose()
	
	local puller = spaceFeedsManager._spacePullers[ spaceUserId1 ]
	
	rxTest:expectNotEqual( nil, puller )
	
	-- Add an asset to the space.

	local assetDoc = buildDocValue( rxTest, wfCatalog, assetDocId )
	assetDoc._spaceId = spaceId1
	assetDoc._newRevId = newRevId1
	assetDoc.develop = nil
	KSTableUtils.setNested( assetDoc, captureDate, 'captureDate' )
	
	rxTest:expectSingleValue( wfCatalog:docStore():setDocValue( assetDoc ), ':ignore:' )
	
	-- Trigger the space metadata pusher.
	
	local sendNUpdates = rxTest:observe( spaceFeedsManager._spacePusher:_DEBUG_sendUpdatesOnce() )
	sendNUpdates:expectCompletedWithinTime( 5 )
	sendNUpdates:dispose()
	
	-- Before assetDoc create response is received, 
	-- we get update that owner entitlementExpired = true 
	
	local inactiveSpaceStream = rxTest:observe( spaceFeedsManager._inactiveSpacesStream )
	
	rxTest:expectOnlyCompleted( spaceFeedsManager:_testOnly_getUpdatesOnce() )
	
	inactiveSpaceStream:expectDataWithinTime( 5, spaceUserId1, true )
	
	inactiveSpaceStream:dispose()
	
	-- spaceFeedsManager should try to shutdwon space pull
	-- Before space puller is shutdown, we would pull update from space one last time
	-- It would mark asset with error TrialOrSubscriptionExpiredError
	
	local errorObserver = rxTest:observe( wfCatalog:docStore():ongoingDocValue( assetDocId )
			:map( function( docValue )
				local errorSubtype = KSTableUtils.derefOpt( docValue, '_localOnly', 'ozError', 'subtype' )
				return errorSubtype
			end ) )
			
	errorObserver:expectDataWithinTime( 10, 'TrialOrSubscriptionExpiredError' )
	
	-- Verify the puller is shut down
	
	puller = spaceFeedsManager._spacePullers[ spaceUserId1 ]
	
	rxTest:expectEqual( nil, puller )

end

--------------------------------------------------------------------------------

function WFCatSpaceFeedsManagerTest.rxTestDowngradedFromEditorPurge( rxTest )

	rxTest:setTimeLimit( 30 )

	local assetDocId = '8462f5ba3b39fd050d3cd6b7352ba561'
	
	local expectedResponses = {
		{
			url = string.format( "/v2/spaces/%s/updates", spaceId1 ),

			response = string.format( [[{
				"base": "https://photos.adobe.io",
				"updates": [ {
						"id": "8462f5ba3b39fd050d3cd6b7352ba561",
						"spaceId": "%s",
						"created": "2015-05-27T00:34:56.669987Z",
						"updated": "2015-05-27T00:38:29.160369Z",
						"type": "asset",
						"subtype": "image",
						"revision_ids": [
							"a1e880f718a348b89663cf38f9f29d23",
							"cd1ab30cf8464ef2a9e736b50c0ba33d",
							"96f33a7215e04c9aa49346bbf8aae921",
							"813fdbda1b244c2d9f41008ba63eedba"
						],
						"editable": true,
						"links": {
							"/rels/master": {
								"href": "/v2/catalog/master"
							}
						},
						"payload": {
							"importSource": {
								"importTimestamp": "2015-05-26T17:34:23-07:00",
								"originalHeight": 1600,
								"originalWidth": 1068
							},
							"captureDate": "2015-05-17T15:50:12",
							"xmp": {
								"dc": {
									"rights": "test copyright string"
								}
							},
							"location": {
								"longitude": -93.600107,
								"latitude": 45.951733,
								"altitude": 377.9
							}
						}
					}
				],
				"links": {
					"next": {
						"href": "/v2/spaces/%s/updates?updated_since=pull1"
					}
				}
			}]], spaceId1, spaceId1 ),

			headers = {
				status_code = '200',
			},

			options = {
				bypassRateLimiter = true,
			}

		},
		{
			url = string.format( "/v2/spaces/%s/updates?updated_since=pull1", spaceId1 ),

			response = string.format( [[{
				"base": "https://photos.adobe.io",
				"updates": [],
				"links": {
					"next": {
						"href": "/v2/spaces/%s/updates?updated_since=pull1"
					}
				}
			}]], spaceId1 ),

			headers = {
				status_code = '200',
			},

			options = {
				bypassRateLimiter = true,
			}

		},
		{
			url = "/v2/spaces/updates?updated_since=pull1",
			
			response = [[{
			  "base": "https://photos-stage.adobe.io/v2/",
			  "updates": [
				{
				  "created": "2019-08-21T17:01:02.467393Z",
				  "type": "space",
				  "subtype": "shared",
				  "id": "<space1_spaceId>",
				  "owner": {
					"id": "<space1_spaceOwnerId>",
					"outOfQuota": false,
					"entitlementExpired": false,
					"links": {
					  "self": {
						"href": "spaces/<space1_spaceId>/updates/<space1_spaceOwnerId>"
					  }
					}
				  },
				  "this_space_user": {
					"id": "<space1_youSpaceUserId>",
					"user": {
					  "id": "<space1_youFeedbackUserId>",
					  "links": {
						"self": {
						  "href": "spaces/<space1_spaceId>/updates/<space1_youFeedbackUserId>"
						}
					  }
					},
					"links": {
					  "self": {
						"href": "spaces/<space1_spaceId>/updates/<space1_youSpaceUserId>"
					  }
					}
				  },
				  "albums": [
					{
					  "id": "<album1_id>",
					  "links": {
						"self": {
						  "href": "spaces/<space1_spaceId>/updates/<album1_id>"
						}
					  },
					  "space_album": {
						"id": "<spaceAlbum1_id>",
						"links": {
						  "self": {
							"href": "spaces/<space1_spaceId>/updates/<spaceAlbum1_id>"
						  }
						}
					  }
					}
				  ],
				  "revision_ids": [
					"33ca44a7da51f56e6d3f9e80b9181aaf",
					"813254e958ea3ffac99d32cda7acc969",
					"b6088deb146acbbde9268c50156fa19a",
					"a0cb906593fa65713d198cec5407426a",
					"3a7c6f13acb6fd7afa9d979681ae1e20",
					"8f8b861e17b67454062ab16b7437b189",
					"2ddfea16ac1a4af1cd0b9a1937fc9c3a",
					"d86b4baa17e22e23bf1a87e84e7d9807",
					"356b4baa17e22e23bf1a87e84e7d9807"
				  ],
				  "updated": "2019-08-22T17:01:08.471614Z",
				  "country_blocked": false,
				  "payload": {
					"userUpdated": "2019-08-21T17:01:08.445000Z",
					"allowInviteRequests": true,
					"userCreated": "2019-08-21T17:01:02.439000Z",
					"presentationFilters": true,
					"location": true,
					"download": true,
					"private": true,
					"metadata": true
				  },
				  "createdOnClient": "Chrome-76.0.3809-MacOSX",
				  "links": {
					"self": {
					  "href": "spaces/<space1_spaceId>/updates/<space1_spaceId>"
					},
					"current": {
					  "href": "spaces/<space1_spaceId>/updates?updated_since=pull2"
					},
					"/rels/web_space": {
					  "href": "http://bit.ly/2ZkCLDo"
					},
					"start": {
					  "href": "spaces/<space1_spaceId>/updates"
					}
				  }
				}
			  ],
			  "links": {
				"next": {
				  "href": "/v2/spaces/updates?updated_since=pull2"
				}
			  }
			}]],
			
			responseFilter = function( response )

				response = response:gsub( '<space1_spaceId>', space1.id )
				response = response:gsub( '<space1_youSpaceUserId>', space1.youSpaceUserId )
				response = response:gsub( '<space1_spaceOwnerId>', space1.ownerId or KSIdGenerator.uuidInLowercaseHex() )
				response = response:gsub( '<space1_youFeedbackUserId>', space1.youFeedbackUserId or KSIdGenerator.uuidInLowercaseHex() ) 
				
				response = response:gsub( '<album1_id>', space1.albumId )
				response = response:gsub( '<spaceAlbum1_id>', space1.spaceAlbumId )

				return response

			end,
			
			headers = {
				status_code = '200',
			},
			
			options = {
				bypassRateLimiter = true,
			}
				
		},
		{
			url = string.format( "/v2/spaces/%s/updates?updated_since=pull1", spaceId1 ),
			
			response = string.format( [[{
				"base": "https://photos.adobe.io",
				"updates": [ {
					"updated": "2019-07-25T22:27:41.466149Z",
					  "type": "space_user",
					  "payload": {
						"status": "accepted",
						"role": "contributor",
						"userUpdated": "2019-07-25T22:27:41.299863Z",
						"userCreated": "2019-07-25T22:27:41.299863Z"
					  },
					  "id": "<space1_youSpaceUserId>",
					  "user": {
						"id": "87bd59dd4c01e4f71155d7bfb50ccdf3"
					  },
					  "created": "2019-07-25T22:27:41.466149Z",
					  "revision_ids": [
						"ae43e159e80148fb47a8fe3ec4e6a718"
					  ]
					}
				],
				"links": {
					"next": {
						"href": "/v2/spaces/%s/updates?updated_since=pull2"
					}
				}
			}]], spaceId1 ),
			
			responseFilter = function( response )

				response = response:gsub( '<space1_spaceId>', space1.id )
				response = response:gsub( '<space1_youSpaceUserId>', space1.youSpaceUserId )

				return response

			end,

			headers = {
				status_code = '200',
			},
			
			options = {
				bypassRateLimiter = true,
			}
			
		},
		
		{
			url = string.format( "/v2/spaces/%s/updates?updated_since=pull2", spaceId1 ),

			response = string.format( [[{
				"base": "https://photos.adobe.io",
				"updates": [],
				"links": {
					"next": {
						"href": "/v2/spaces/%s/updates?updated_since=pull2"
					}
				}
			}]], spaceId1 ),

			headers = {
				status_code = '200',
			},

			options = {
				bypassRateLimiter = true,
			}

		},

	}
	
	local args = {
		spaceUser1Role = 'editor'
	}
	
	local catalogSpec = createCatalogSpecForTest( rxTest, space1, nil, expectedResponses, args )

	local wfCatalog = createWFCatalogForTestWithSpec( rxTest, catalogSpec )
	
	local spaceFeedsManagerArgs = {
		taskDelays = {
			progressiveBackoff = false,
			noWorkDelay = 0.5,
			maxNoWorkDelay = 1,
			errorDelay = 1,
			maxDelay = 1,
		},
		albumAssetsForFilteredAssetsQueryFn = albumAssetsForFilteredAssetsQueryFn,
		testMode = true
	}

	-- Start up the space feed manager

	local spaceFeedsManager = WFCatSpaceFeedsManager( wfCatalog, spaceFeedsManagerArgs )
	rxTest:addCloseTarget( spaceFeedsManager )
	
	-- Verify the active spaces.
	
	local activeSpacesObserver = rxTest:observe( spaceFeedsManager._activeSpacesStream )
	
	local isDownloadingObserver = rxTest:observe( spaceFeedsManager:isDownloadingMetadata() )
	
	rxTest:expectOnlyCompleted( spaceFeedsManager:_testOnly_getUpdatesOnce() )
	
	activeSpacesObserver:expectDataWithinTime( 2, spaceId1, spaceUserId1, ":ignore:", false, ":ignore:", ":ignore:" )
	
	isDownloadingObserver:expectDataWithinTime( 5, true ) -- Space 1
	isDownloadingObserver:expectDataWithinTime( 5, false )
	
	isDownloadingObserver:dispose()
	
	-- Check asset and add album_asset doc
	
	rxTest:expectSingleValue( wfCatalog:docStore():docValue( assetDocId )
		:map( function( docValue )
			
			local editable = not not KSTableUtils.derefOpt( docValue, '_serverProvidedMetadata', 'editable' )
			local masterLink = not not  KSTableUtils.derefOpt( docValue, '_serverProvidedMetadata', 'links', '/rels/master', 'href' )
			
			return editable and masterLink
			
		end )
		, true
	)
	
	local albumAssetDoc = {

		_id = KSIdGenerator.uuidInLowercaseHex(),
		_spaceId = spaceId1,
		type = 'album_asset',
		asset = {
			id = assetDocId
		},
		album = {
			id = albumId1
		}
	}
	
	rxTest:waitFor( wfCatalog:docStore():setDocValue( albumAssetDoc ) )
	
	-- Simulate original file is downloaded

	local dummyDocValue, expectedData = buildDocValue( rxTest, wfCatalog )

	local masterField
	local originalField
	local originalFilePathField
	
	if WFPlatformInfo.isDesktop then
		originalField = KSTableUtils.derefOpt( dummyDocValue, '_localOnly', 'files', 'original' )
	elseif WFPlatformInfo.isiOS then
		masterField = KSTableUtils.derefOpt( dummyDocValue, '_localOnly', 'files', 'master' )
	elseif WFPlatformInfo.isAndroid then
		originalFilePathField = KSTableUtils.derefOpt( dummyDocValue, '_localOnly', 'originalFilePath' )
	end
	
	rxTest:waitFor( wfCatalog:docStore():applyOverlay{
		_id = assetDocId,
		_localOnly = {
			files = {
				original = originalField,
				master = masterField
			},
			originalFilePath = originalFilePathField
		}
	} )
	
	-- Get more updates. This should update spaceUser1 to a contributor
	
	local downgradedFromEditorRoleObserver = rxTest:observe( wfCatalog:docStore():ongoingDocValue( spaceUserId1 )
		:map( function( spaceUserDoc ) 
			return KSTableUtils.derefOpt( spaceUserDoc, '_localOnly', 'universal', 'downgradedFromEditorRole' )
		end ) )
	
	rxTest:expectOnlyCompleted( spaceFeedsManager:_testOnly_getUpdatesOnce() )
	
	activeSpacesObserver:expectDataWithinTime( 2, spaceId1, spaceUserId1, ":ignore:", false, ":ignore:", ":ignore:" )
	
	activeSpacesObserver:dispose()
	
	-- Make sure downgradedFromEditorRole flag is cleared
	
	downgradedFromEditorRoleObserver:expectDataWithinTime( 5, true )
	downgradedFromEditorRoleObserver:expectDataWithinTime( 10, nil )
	
	downgradedFromEditorRoleObserver:dispose()
	
	-- Make sure file is removed and asset doc is updated
	
	rxTest:expectEqual( false, WFFileUtils.exists( expectedData.originalFilePath ) )
	
	rxTest:expectSingleValue( wfCatalog:docStore():docValue( assetDocId )
		:map( function( docValue )
			
			return WFCatAssetUtils.derefUniversal( docValue, 'files', 'original', 'master', 'ralativePath' ) == nil
			
		end )
		, true
	)
	
end

--------------------------------------------------------------------------------

function WFCatSpaceFeedsManagerTest.rxTestDowngradedFromEditorPurge_lossCacheRole( rxTest )

	rxTest:setTimeLimit( 30 )

	local assetDocId = '8462f5ba3b39fd050d3cd6b7352ba561'
	
	local expectedResponses = {
		{
			url = string.format( "/v2/spaces/%s/updates", spaceId1 ),

			response = string.format( [[{
				"base": "https://photos.adobe.io",
				"updates": [ {
						"id": "8462f5ba3b39fd050d3cd6b7352ba561",
						"spaceId": "%s",
						"created": "2015-05-27T00:34:56.669987Z",
						"updated": "2015-05-27T00:38:29.160369Z",
						"type": "asset",
						"subtype": "image",
						"revision_ids": [
							"a1e880f718a348b89663cf38f9f29d23",
							"cd1ab30cf8464ef2a9e736b50c0ba33d",
							"96f33a7215e04c9aa49346bbf8aae921",
							"813fdbda1b244c2d9f41008ba63eedba"
						],
						"editable": true,
						"links": {
							"/rels/master": {
								"href": "/v2/catalog/master"
							}
						},
						"payload": {
							"importSource": {
								"importTimestamp": "2015-05-26T17:34:23-07:00",
								"originalHeight": 1600,
								"originalWidth": 1068
							},
							"captureDate": "2015-05-17T15:50:12",
							"xmp": {
								"dc": {
									"rights": "test copyright string"
								}
							},
							"location": {
								"longitude": -93.600107,
								"latitude": 45.951733,
								"altitude": 377.9
							}
						}
					}
				],
				"links": {
					"next": {
						"href": "/v2/spaces/%s/updates?updated_since=pull1"
					}
				}
			}]], spaceId1, spaceId1 ),

			headers = {
				status_code = '200',
			},

			options = {
				bypassRateLimiter = true,
			}

		},
		{
			url = string.format( "/v2/spaces/%s/updates?updated_since=pull1", spaceId1 ),

			response = string.format( [[{
				"base": "https://photos.adobe.io",
				"updates": [],
				"links": {
					"next": {
						"href": "/v2/spaces/%s/updates?updated_since=pull1"
					}
				}
			}]], spaceId1 ),

			headers = {
				status_code = '200',
			},

			options = {
				bypassRateLimiter = true,
			}

		},
	}
	
	local args = {
		spaceUser1Role = 'editor'
	}
	
	local catalogSpec = createCatalogSpecForTest( rxTest, space1, nil, expectedResponses, args )

	local wfCatalog = createWFCatalogForTestWithSpec( rxTest, catalogSpec )
	
	local spaceFeedsManagerArgs = {
		taskDelays = {
			progressiveBackoff = false,
			noWorkDelay = 0.5,
			maxNoWorkDelay = 1,
			errorDelay = 1,
			maxDelay = 1,
		},
		albumAssetsForFilteredAssetsQueryFn = albumAssetsForFilteredAssetsQueryFn,
		testMode = true
	}

	-- Start up the space feed manager

	local spaceFeedsManager = WFCatSpaceFeedsManager( wfCatalog, spaceFeedsManagerArgs )
	
	-- Verify the active spaces.
	
	local activeSpacesObserver = rxTest:observe( spaceFeedsManager._activeSpacesStream )
	
	local isDownloadingObserver = rxTest:observe( spaceFeedsManager:isDownloadingMetadata() )
	
	rxTest:expectOnlyCompleted( spaceFeedsManager:_testOnly_getUpdatesOnce() )
	
	activeSpacesObserver:expectDataWithinTime( 2, spaceId1, spaceUserId1, ":ignore:", false, ":ignore:", ":ignore:" )
	
	isDownloadingObserver:expectDataWithinTime( 5, true ) -- Space 1
	isDownloadingObserver:expectDataWithinTime( 5, false )
	
	isDownloadingObserver:dispose()
	activeSpacesObserver:dispose()
	
	-- Check asset and add album_asset doc
	
	rxTest:expectSingleValue( wfCatalog:docStore():docValue( assetDocId )
		:map( function( docValue )
			
			local editable = not not KSTableUtils.derefOpt( docValue, '_serverProvidedMetadata', 'editable' )
			local masterLink = not not  KSTableUtils.derefOpt( docValue, '_serverProvidedMetadata', 'links', '/rels/master', 'href' )
			
			return editable and masterLink
			
		end )
		, true
	)
	
	local albumAssetDoc = {

		_id = KSIdGenerator.uuidInLowercaseHex(),
		_spaceId = spaceId1,
		type = 'album_asset',
		asset = {
			id = assetDocId
		},
		album = {
			id = albumId1
		}
	}
	
	rxTest:waitFor( wfCatalog:docStore():setDocValue( albumAssetDoc ) )
	
	-- Simulate original file is downloaded

	local dummyDocValue, expectedData = buildDocValue( rxTest, wfCatalog )

	local masterField
	local originalField
	local originalFilePathField
	
	if WFPlatformInfo.isDesktop then
		originalField = KSTableUtils.derefOpt( dummyDocValue, '_localOnly', 'files', 'original' )
	elseif WFPlatformInfo.isiOS then
		masterField = KSTableUtils.derefOpt( dummyDocValue, '_localOnly', 'files', 'master' )
	elseif WFPlatformInfo.isAndroid then
		originalFilePathField = KSTableUtils.derefOpt( dummyDocValue, '_localOnly', 'originalFilePath' )
	end
	
	rxTest:waitFor( wfCatalog:docStore():applyOverlay{
		_id = assetDocId,
		_localOnly = {
			files = {
				original = originalField,
				master = masterField
			},
			originalFilePath = originalFilePathField
		}
	} )
	
	-- close spaceFeedsManager
	
	spaceFeedsManager:close()
	
	-- Update spaceUser role to contributor and add downgradedFromEditorRole flag
	
	rxTest:waitFor( wfCatalog:docStore():applyOverlay{
		_id = spaceUserId1,
		role = 'contributor',
		_localOnly = {
			universal = {
				downgradedFromEditorRole = true
			}
		}
	} )
	
	local downgradedFromEditorRoleObserver = rxTest:observe( wfCatalog:docStore():ongoingDocValue( spaceUserId1 )
		:map( function( spaceUserDoc ) 
			return KSTableUtils.derefOpt( spaceUserDoc, '_localOnly', 'universal', 'downgradedFromEditorRole' )
		end ) )
	
	downgradedFromEditorRoleObserver:expectDataWithinTime( 5, true )
	
	-- create spaceFeedsManager again
	
	spaceFeedsManager = WFCatSpaceFeedsManager( wfCatalog, spaceFeedsManagerArgs )
	rxTest:addCloseTarget( spaceFeedsManager )
	
	-- Make sure downgradedFromEditorRole flag is removed
	
	downgradedFromEditorRoleObserver:expectDataWithinTime( 10, nil )
	
	downgradedFromEditorRoleObserver:dispose()
	
	-- Make sure file is removed and asset doc is updated
	
	rxTest:expectEqual( false, WFFileUtils.exists( expectedData.originalFilePath ) )
	
	rxTest:expectSingleValue( wfCatalog:docStore():docValue( assetDocId )
		:map( function( docValue )
			
			return WFCatAssetUtils.derefUniversal( docValue, 'files', 'original', 'master', 'ralativePath' ) == nil
			
		end )
		, true
	)
		
end

--------------------------------------------------------------------------------

function WFCatSpaceFeedsManagerTest.rxTestDowngradedFromEditor_removeUseOfflineFlag( rxTest )

	rxTest:setTimeLimit( 30 )

	local expectedResponses = {
		{
			url = string.format( "/v2/spaces/%s/updates", spaceId1 ),

			response = string.format( [[{
				"base": "https://photos.adobe.io",
				"updates": [],
				"links": {
					"next": {
						"href": "/v2/spaces/%s/updates?updated_since=pull1"
					}
				}
			}]], spaceId1 ),

			headers = {
				status_code = '200',
			},

			options = {
				bypassRateLimiter = true,
			}

		},
		{
			url = "/v2/spaces/updates?updated_since=pull1",
			
			response = [[{
			  "base": "https://photos-stage.adobe.io/v2/",
			  "updates": [
				{
				  "created": "2019-08-21T17:01:02.467393Z",
				  "type": "space",
				  "subtype": "shared",
				  "id": "<space1_spaceId>",
				  "owner": {
					"id": "<space1_spaceOwnerId>",
					"outOfQuota": false,
					"entitlementExpired": false,
					"links": {
					  "self": {
						"href": "spaces/<space1_spaceId>/updates/<space1_spaceOwnerId>"
					  }
					}
				  },
				  "this_space_user": {
					"id": "<space1_youSpaceUserId>",
					"user": {
					  "id": "<space1_youFeedbackUserId>",
					  "links": {
						"self": {
						  "href": "spaces/<space1_spaceId>/updates/<space1_youFeedbackUserId>"
						}
					  }
					},
					"links": {
					  "self": {
						"href": "spaces/<space1_spaceId>/updates/<space1_youSpaceUserId>"
					  }
					}
				  },
				  "albums": [
					{
					  "id": "<album1_id>",
					  "links": {
						"self": {
						  "href": "spaces/<space1_spaceId>/updates/<album1_id>"
						}
					  },
					  "space_album": {
						"id": "<spaceAlbum1_id>",
						"links": {
						  "self": {
							"href": "spaces/<space1_spaceId>/updates/<spaceAlbum1_id>"
						  }
						}
					  }
					}
				  ],
				  "revision_ids": [
					"33ca44a7da51f56e6d3f9e80b9181aaf",
					"813254e958ea3ffac99d32cda7acc969",
					"b6088deb146acbbde9268c50156fa19a",
					"a0cb906593fa65713d198cec5407426a",
					"3a7c6f13acb6fd7afa9d979681ae1e20",
					"8f8b861e17b67454062ab16b7437b189",
					"2ddfea16ac1a4af1cd0b9a1937fc9c3a",
					"d86b4baa17e22e23bf1a87e84e7d9807",
					"356b4baa17e22e23bf1a87e84e7d9807"
				  ],
				  "updated": "2019-08-22T17:01:08.471614Z",
				  "country_blocked": false,
				  "payload": {
					"userUpdated": "2019-08-21T17:01:08.445000Z",
					"allowInviteRequests": true,
					"userCreated": "2019-08-21T17:01:02.439000Z",
					"presentationFilters": true,
					"location": true,
					"download": true,
					"private": true,
					"metadata": true
				  },
				  "createdOnClient": "Chrome-76.0.3809-MacOSX",
				  "links": {
					"self": {
					  "href": "spaces/<space1_spaceId>/updates/<space1_spaceId>"
					},
					"current": {
					  "href": "spaces/<space1_spaceId>/updates?updated_since=pull2"
					},
					"/rels/web_space": {
					  "href": "http://bit.ly/2ZkCLDo"
					},
					"start": {
					  "href": "spaces/<space1_spaceId>/updates"
					}
				  }
				}
			  ],
			  "links": {
				"next": {
				  "href": "/v2/spaces/updates?updated_since=pull2"
				}
			  }
			}]],
			
			responseFilter = function( response )

				response = response:gsub( '<space1_spaceId>', space1.id )
				response = response:gsub( '<space1_youSpaceUserId>', space1.youSpaceUserId )
				response = response:gsub( '<space1_spaceOwnerId>', space1.ownerId or KSIdGenerator.uuidInLowercaseHex() )
				response = response:gsub( '<space1_youFeedbackUserId>', space1.youFeedbackUserId or KSIdGenerator.uuidInLowercaseHex() ) 
				
				response = response:gsub( '<album1_id>', space1.albumId )
				response = response:gsub( '<spaceAlbum1_id>', space1.spaceAlbumId )

				return response

			end,
			
			headers = {
				status_code = '200',
			},
			
			options = {
				bypassRateLimiter = true,
			}
				
		},
		{
			url = string.format( "/v2/spaces/%s/updates?updated_since=pull1", spaceId1 ),
			
			response = string.format( [[{
				"base": "https://photos.adobe.io",
				"updates": [ {
					"updated": "2019-07-25T22:27:41.466149Z",
					  "type": "space_user",
					  "payload": {
						"status": "accepted",
						"role": "contributor",
						"userUpdated": "2019-07-25T22:27:41.299863Z",
						"userCreated": "2019-07-25T22:27:41.299863Z"
					  },
					  "id": "<space1_youSpaceUserId>",
					  "user": {
						"id": "87bd59dd4c01e4f71155d7bfb50ccdf3"
					  },
					  "created": "2019-07-25T22:27:41.466149Z",
					  "revision_ids": [
						"ae43e159e80148fb47a8fe3ec4e6a718"
					  ]
					}
				],
				"links": {
					"next": {
						"href": "/v2/spaces/%s/updates?updated_since=pull2"
					}
				}
			}]], spaceId1 ),
			
			responseFilter = function( response )

				response = response:gsub( '<space1_spaceId>', space1.id )
				response = response:gsub( '<space1_youSpaceUserId>', space1.youSpaceUserId )

				return response

			end,

			headers = {
				status_code = '200',
			},
			
			options = {
				bypassRateLimiter = true,
			}
			
		},
		
		{
			url = string.format( "/v2/spaces/%s/updates?updated_since=pull2", spaceId1 ),

			response = string.format( [[{
				"base": "https://photos.adobe.io",
				"updates": [],
				"links": {
					"next": {
						"href": "/v2/spaces/%s/updates?updated_since=pull2"
					}
				}
			}]], spaceId1 ),

			headers = {
				status_code = '200',
			},

			options = {
				bypassRateLimiter = true,
			}

		},

	}
	
	local args = {
		spaceUser1Role = 'editor'
	}
	
	local catalogSpec = createCatalogSpecForTest( rxTest, space1, nil, expectedResponses, args )

	local wfCatalog = createWFCatalogForTestWithSpec( rxTest, catalogSpec )
	
	local spaceFeedsManagerArgs = {
		taskDelays = {
			progressiveBackoff = false,
			noWorkDelay = 0.5,
			maxNoWorkDelay = 1,
			errorDelay = 1,
			maxDelay = 1,
		},
		albumAssetsForFilteredAssetsQueryFn = albumAssetsForFilteredAssetsQueryFn,
		testMode = true
	}

	-- Start up the space feed manager

	local spaceFeedsManager = WFCatSpaceFeedsManager( wfCatalog, spaceFeedsManagerArgs )
	rxTest:addCloseTarget( spaceFeedsManager )
	
	-- Verify the active spaces.
	
	local activeSpacesObserver = rxTest:observe( spaceFeedsManager._activeSpacesStream )
	
	local isDownloadingObserver = rxTest:observe( spaceFeedsManager:isDownloadingMetadata() )
	
	rxTest:expectOnlyCompleted( spaceFeedsManager:_testOnly_getUpdatesOnce() )
	
	activeSpacesObserver:expectDataWithinTime( 2, spaceId1, spaceUserId1, ":ignore:", false, ":ignore:", ":ignore:" )
	
	isDownloadingObserver:expectDataWithinTime( 5, true ) -- Space 1
	isDownloadingObserver:expectDataWithinTime( 5, false )
	
	isDownloadingObserver:dispose()
	
	-- Check album doc
	
	rxTest:expectSingleValue( wfCatalog:docStore():docValue( albumId1 )
		:map( function( albumDoc )
			return not albumDoc._empty and albumDoc.name ~= nil
		end ), true )
	
	-- Add use offline flag to album
	
	rxTest:waitFor( wfCatalog:docStore():applyOverlay{
			_id = albumId1,
			_localOnly = {
				isMarkedForOfflineUse = WFPlatformInfo.isDesktop and true or nil,
				useOffline = WFPlatformInfo.isMobile() and true or nil
			}
		} )
		
	if WFPlatformInfo.isDesktop then
		
		rxTest:observe( wfCatalog:indexStore():query{
				sql = "SELECT albumId, isMarkedForOffline FROM albums WHERE spaceId == ?", spaceId1,
			} )
			:expectDataWithinTime( 3, {{ albumId1, 1 }} )
			:dispose()
		
	else
		
		rxTest:observe( wfCatalog:indexStore():query{
				sql = "SELECT docId, useOffline FROM albums WHERE spaceId == ?", spaceId1,
			} )
			:expectDataWithinTime( 3, {{ albumId1, 1 }} )
			:dispose()
		
	end
	
	-- Get more updates. This should update spaceUser1 to a contributor
	
	local albumUserOfflineFlagObserver = rxTest:observe( wfCatalog:docStore():ongoingDocValue( albumId1 )
		:map( function( albumDoc )
			local useOfflineFlagName = WFPlatformInfo.isDesktop and "isMarkedForOfflineUse" or "useOffline"
			return KSTableUtils.derefOpt( albumDoc, '_localOnly', useOfflineFlagName )
		end ) )
		
	albumUserOfflineFlagObserver:expectData( true )
	
	rxTest:expectOnlyCompleted( spaceFeedsManager:_testOnly_getUpdatesOnce() )
	
	activeSpacesObserver:expectDataWithinTime( 2, spaceId1, spaceUserId1, ":ignore:", false, ":ignore:", ":ignore:" )
	
	activeSpacesObserver:dispose()
	
	-- Make sure use offline flag is cleared
	
	albumUserOfflineFlagObserver:expectDataWithinTime( 10, nil )
	
	albumUserOfflineFlagObserver:dispose()
	
end

--------------------------------------------------------------------------------

function WFCatSpaceFeedsManagerTest.rxTestBasicSpaceFeedsManagerOz_testNightly( rxTest )

	rxTest:setTimeLimit( 120 )
	
	local _, _, _, memberWFCatalog, _, _, _, spaceUserId = WFCatOzTestSession.setupSpaceOzTest( rxTest, 'viewer', nil, true )
			
	local spaceFeedsManagerArgs = {
		taskDelays = {
			progressiveBackoff = false,
			noWorkDelay = 0.5,
			maxNoWorkDelay = 1,
			errorDelay = 1,
			maxDelay = 1,
		},
		isPrefetchEnabled = true,
		albumAssetsForFilteredAssetsQueryFn = albumAssetsForFilteredAssetsQueryFn,
		testMode = true,
	}
	
	local spaceFeedsManager = WFCatSpaceFeedsManager( memberWFCatalog, spaceFeedsManagerArgs )
	rxTest:addCloseTarget( spaceFeedsManager )
	
	local observer = rxTest:observe( spaceFeedsManager:isDownloadingMetadata() )
	
	spaceFeedsManager:reportCatalogInitialSyncIsComplete()
	spaceFeedsManager:startFetchingUpdates()
	spaceFeedsManager:startSendingUpdates()
	
	observer:expectDataWithinTime( 60, true )
	observer:expectDataWithinTime( 10, false )
	
	observer:dispose()
	
	-- Note that the asset nor the album_asset show up on the sync feed because
	-- we did not upload the original for the asset from the owner's account.
	
	local expectedLogData = {
		documentsReceivedByType = {
			update = {
				album = {
					collection = 1,
				},
				feedbackuser = {
					NULL = 2,
				},
				space_album = {
					NULL = 1,
				},
				space_user = {
					NULL = 1,
				},
				total = 5,
			},
		},
		successfulUpdateCycles = 2,
		totalDocumentsReceived = 5,
	}

	local spacePuller = spaceFeedsManager._spacePullers[ spaceUserId ]
	
	verifyLogData( rxTest, spacePuller:getDiagnosticLogData(), expectedLogData, 'ignore', spaceUserId1 )
	
end

--------------------------------------------------------------------------------

function WFCatSpaceFeedsManagerTest.rxTestBasicSpaceFeedsManagerOz_withAsset_testNightly( rxTest )

	rxTest:setTimeLimit( 120 )
	
	local _, _, _, memberWFCatalog, _, _, _, spaceUserId = WFCatOzTestSession.setupSpaceOzTest( rxTest, 'viewer', nil, true )
	
	-- It takes about 1 min for Oz to add asset and album_asset doc into space feeds
	
	rxTest:sleep( 60 )
	
	local spaceFeedsManagerArgs = {
		taskDelays = {
			progressiveBackoff = false,
			noWorkDelay = 0.5,
			maxNoWorkDelay = 1,
			errorDelay = 1,
			maxDelay = 1,
		},
		isPrefetchEnabled = true,
		albumAssetsForFilteredAssetsQueryFn = albumAssetsForFilteredAssetsQueryFn,
		testMode = true,
	}
	
	local spaceFeedsManager = WFCatSpaceFeedsManager( memberWFCatalog, spaceFeedsManagerArgs )
	rxTest:addCloseTarget( spaceFeedsManager )
	
	local observer = rxTest:observe( spaceFeedsManager:isDownloadingMetadata() )
	
	spaceFeedsManager:reportCatalogInitialSyncIsComplete()
	spaceFeedsManager:startFetchingUpdates()
	spaceFeedsManager:startSendingUpdates()
	
	observer:expectDataWithinTime( 60, true )
	observer:expectDataWithinTime( 10, false )
	
	observer:dispose()
	
	local expectedLogData = {
		documentsReceivedByType = {
			update = {
				album = {
					collection = 1,
				},
				album_asset = {
					NULL = 1,
				},
				asset = {
					image = 1,
				},
				feedbackuser = {
					NULL = 2,
				},
				space_album = {
					NULL = 1,
				},
				space_user = {
					NULL = 1,
				},
				total = 7,
			},
		},
		successfulUpdateCycles = 2,
		totalDocumentsReceived = 7,
	}

	local spacePuller = spaceFeedsManager._spacePullers[ spaceUserId ]
	
	verifyLogData( rxTest, spacePuller:getDiagnosticLogData(), expectedLogData, 'ignore', spaceUserId1 )
	
end

--------------------------------------------------------------------------------

function WFCatSpaceFeedsManagerTest.rxTestSpaceEditorCompressionListHandler_testNightly( rxTest )

	rxTest:setTimeLimit( 300 )
	
	local _, ownerCatalogSpec, ownerCatalogTestSession,
			memberWFCatalog, memberCatalogSpec, memberCatalogTestSession,
			spaceId, _, _, assetId = WFCatOzTestSession.setupSpaceOzTest( rxTest, 'editor', nil, true )	
	
	local spaceFeedsManagerArgs = {
		taskDelays = {
			progressiveBackoff = false,
			noWorkDelay = 0.5,
			maxNoWorkDelay = 1,
			errorDelay = 1,
			maxDelay = 1,
		},
		isPrefetchEnabled = true,
		albumAssetsForFilteredAssetsQueryFn = albumAssetsForFilteredAssetsQueryFn,
		testMode = true,
	}
	
	local spaceFeedsManager = WFCatSpaceFeedsManager( memberWFCatalog, spaceFeedsManagerArgs )
	rxTest:addCloseTarget( spaceFeedsManager )
	
	spaceFeedsManager:reportCatalogInitialSyncIsComplete()
	spaceFeedsManager:startFetchingUpdates()
	spaceFeedsManager:startSendingUpdates()
	
	local memberAssetId
	
	local querySpaceAssetIdStr
	if WFPlatformInfo.isDesktop then
		querySpaceAssetIdStr = [[SELECT album_asset_v2.assetId 
								FROM albums INNER JOIN album_asset_v2 ON albums.albumId == album_asset_v2.albumId
								WHERE spaceId == ?]]
	else
		querySpaceAssetIdStr = [[SELECT album_asset.assetId
								FROM albums INNER JOIN album_asset ON albums.docId == album_asset.albumId 
								WHERE spaceId == ?]]
	end
	
	rxTest:observe( 
		memberWFCatalog:indexStore():query{
				sql = querySpaceAssetIdStr, spaceId,
				singleColumnQuery = true
			}
			:map( function( assetIds )
				memberAssetId = assetIds[ 1 ]
				return #assetIds == 1
			end ) )
		:expectDataWithinTime( 90, true )
	
	spaceFeedsManager:stopFetchingUpdates()
	spaceFeedsManager:stopSendingUpdates()
	
	-- Check if assetDoc is editable
	
	rxTest:expectSingleValue( memberWFCatalog:docStore():docValue( memberAssetId )
		:map( function( assetDoc )
			return WFCatSharedWithYouUtils.canEditAsset( assetDoc )
		end ),
		true
	)
	
	-- Get latest asset revision catalog_id
	
	local assetDocGetUrl = string.format( '/v2/spaces/%s/assets/%s',
											spaceId,
											assetId )
											
	local assetDocGetArgs = {
		requestHeaders = {
			{ header = 'Content-Type', value = 'application/json' },
			{ header = 'Accept', value = 'application/json' },
		},
	}
	
	local assetDocRevId
	
	rxTest:waitFor( 
		memberCatalogSpec:get( assetDocGetUrl, assetDocGetArgs )
			:map( function( data, header )
				
				local assetDoc = KSJsonDecoder.decodeWebSafeJson_stripNull( data )
				
				local revision_ids = KSTableUtils.derefOpt( assetDoc, 'revision_ids' )
				assetDocRevId = revision_ids[ #revision_ids ]
				
			end ) )
	
	
	-- Editor edit the asset document several times and make sure those edits come through.
	
	local assetDocPutArgs = {
		requestHeaders = {
			{ header = 'Content-Type', value = 'application/json' },
			{ header = 'Accept', value = 'application/json' },
		},
	}
	
	local assetDocRev2Id = KSIdGenerator.uuidInLowercaseHex()

	local assetDocPutUrl2 = string.format( '/v2/spaces/%s/assets/%s/revisions/%s/children/%s',
											spaceId,
											assetId,
											assetDocRevId,
											assetDocRev2Id )

	local assetDocContent2 = KSJsonEncoder.encodeOzUploadJson {
		user_id = memberCatalogSpec:wfOzUser():userId(),
		payload = {
			xmp = {
				dc = {
					description = "mumble mumble mumble",
				},
			},
		},
	}

	rxTest:waitFor( memberCatalogSpec:put( assetDocPutUrl2, assetDocContent2, assetDocPutArgs ) )
			
	local assetDocRev3Id = KSIdGenerator.uuidInLowercaseHex()

	local assetDocPutUrl3 = string.format( '/v2/spaces/%s/assets/%s/revisions/%s/children/%s',
											spaceId,
											assetId,
											assetDocRev2Id,
											assetDocRev3Id )

	local assetDocContent3 = KSJsonEncoder.encodeOzUploadJson {
		user_id = memberCatalogSpec:wfOzUser():userId(),
		payload = {
			xmp = {
				dc = {
					description = "description version 3",
				},
			},
		},
	}

	rxTest:waitFor( memberCatalogSpec:put( assetDocPutUrl3, assetDocContent3, assetDocPutArgs ) )


	local assetDocRev4Id = KSIdGenerator.uuidInLowercaseHex()

	local assetDocPutUrl4 = string.format( '/v2/spaces/%s/assets/%s/revisions/%s/children/%s',
											spaceId,
											assetId,
											assetDocRev3Id,
											assetDocRev4Id )

	local assetDocContent4 = KSJsonEncoder.encodeOzUploadJson {
		user_id = memberCatalogSpec:wfOzUser():userId(),
		payload = {
			xmp = {
				dc = {
					description = "description version 4",
				},
			},
		},
	}

	rxTest:waitFor( memberCatalogSpec:put( assetDocPutUrl4, assetDocContent4, assetDocPutArgs ) )
	
	-- Now check asset document from space feed

	spaceFeedsManager:startFetchingUpdates()
	spaceFeedsManager:startSendingUpdates()
	
	rxTest:observe( 
		memberWFCatalog:docStore():ongoingDocValue( memberAssetId )
			:map( function( assetDoc )
				
				local revision_ids = KSTableUtils.derefOpt( assetDoc, '_serverProvidedMetadata', 'revision_ids' )
				
				return #revision_ids == 5
					and KSTableUtils.derefOpt( assetDoc, 'xmp', 'dc', 'description' ) == "description version 4"
				
			end ) )
		:expectDataWithinTime( 90, true )
	
	spaceFeedsManager:stopFetchingUpdates()
	spaceFeedsManager:stopSendingUpdates()
	
	-- Switch to owner catalog
	
	rxTest:expectOnlyCompleted( ownerCatalogTestSession:reloadAccessAndRefreshTokens() )
	
	-- Make one more change remote from owner

	local assetDocRev5Id = KSIdGenerator.uuidInLowercaseHex()

	local assetDocPutUrl5 = string.format( '%s/assets/%s/revisions/%s/children/%s',
										   ownerCatalogSpec:catalogHref(),
										   assetId,
										   assetDocRev4Id,
										   assetDocRev5Id )

	local assetDocContent5 = KSJsonEncoder.encodeOzUploadJson {
		user_id = ownerCatalogSpec:wfOzUser():userId(),
		payload = {
			xmp = {
				dc = {
					description = "description version 5",
				},
			},
		},
	}

	rxTest:waitFor( ownerCatalogSpec:put( assetDocPutUrl5, assetDocContent5, assetDocPutArgs ) )
			
	-- Force a RevisionCompressed doc for this asset from owner
	
	local accessToken = ownerCatalogTestSession._userTestSession._accessToken
	
	local postParams = {
		requestMethod = 'POST',
		requestHeaders = {
			{ header = 'Authorization', value = 'Bearer ' .. accessToken },
			{ header = 'Content-Type', value = 'application/json' },
		}
	}
	
	local compressUrl = string.format( '%s/assets/%s/compress',
										ownerCatalogSpec:catalogHref(), assetId )
	
	local compressFullUrl = 'https://admin.adbephotos-stage.com' .. compressUrl
	
	rxTest:waitFor( WFRxHttpClient.put(compressFullUrl, '{}', postParams ) )

	-- switch back to member
	
	rxTest:expectOnlyCompleted( memberCatalogTestSession:reloadAccessAndRefreshTokens() )
	
	-- Make sure revisions are compressed

	spaceFeedsManager:startFetchingUpdates()
	spaceFeedsManager:startSendingUpdates()

	rxTest:observe( 
		memberWFCatalog:docStore():ongoingDocValue( memberAssetId )
			:map( function( assetDoc )
				
				local revision_ids = KSTableUtils.derefOpt( assetDoc, '_serverProvidedMetadata', 'revision_ids' )
				
				return #revision_ids == 2
					and KSTableUtils.derefOpt( assetDoc, 'xmp', 'dc', 'description' ) == "description version 5"
				
			end ) )
		:expectDataWithinTime( 90, true )
	
end

--------------------------------------------------------------------------------

function WFCatSpaceFeedsManagerTest.rxTestSpaceEditorCompressionListHandler_withDirectAPI_testNightly( rxTest )

	rxTest:setTimeLimit( 120 )
	
	local _, ownerCatalogSpec, ownerCatalogTestSession,
			_, memberCatalogSpec, memberCatalogTestSession,
			spaceId, _, _, assetId = WFCatOzTestSession.setupSpaceOzTest( rxTest, 'editor', nil, true )	
	
	-- Get assset doc. Check if asset is editable to editor
	
	local assetDocGetUrl = string.format( '/v2/spaces/%s/assets/%s',
											spaceId,
											assetId )
											
	local assetDocGetArgs = {
		requestHeaders = {
			{ header = 'Content-Type', value = 'application/json' },
			{ header = 'Accept', value = 'application/json' },
		},
	}
	
	local assetDocRevId
	
	rxTest:expectSingleValue( 
		memberCatalogSpec:get( assetDocGetUrl, assetDocGetArgs )
			:map( function( data, header )
				
				local assetDoc = KSJsonDecoder.decodeWebSafeJson_stripNull( data )
				
				local revision_ids = KSTableUtils.derefOpt( assetDoc, 'revision_ids' )
				assetDocRevId = revision_ids[ #revision_ids ]
				
				return KSTableUtils.derefOpt( assetDoc, 'links', '/rels/master', 'href' ) ~= nil
					and KSTableUtils.derefOpt( assetDoc, 'editable' ) == true
				
			end ),
		true )
	
	-- Editor edit the asset document several times and make sure those edits come through.
	
	local assetDocPutArgs = {
		requestHeaders = {
			{ header = 'Content-Type', value = 'application/json' },
			{ header = 'Accept', value = 'application/json' },
		},
	}
	
	local assetDocRev2Id = KSIdGenerator.uuidInLowercaseHex()

	local assetDocPutUrl2 = string.format( '/v2/spaces/%s/assets/%s/revisions/%s/children/%s',
											spaceId,
											assetId,
											assetDocRevId,
											assetDocRev2Id )

	local assetDocContent2 = KSJsonEncoder.encodeOzUploadJson {
		user_id = memberCatalogSpec:wfOzUser():userId(),
		payload = {
			xmp = {
				dc = {
					description = "mumble mumble mumble",
				},
			},
		},
	}

	rxTest:waitFor( memberCatalogSpec:put( assetDocPutUrl2, assetDocContent2, assetDocPutArgs ) )
			
	local assetDocRev3Id = KSIdGenerator.uuidInLowercaseHex()

	local assetDocPutUrl3 = string.format( '/v2/spaces/%s/assets/%s/revisions/%s/children/%s',
											spaceId,
											assetId,
											assetDocRev2Id,
											assetDocRev3Id )

	local assetDocContent3 = KSJsonEncoder.encodeOzUploadJson {
		user_id = memberCatalogSpec:wfOzUser():userId(),
		payload = {
			xmp = {
				dc = {
					description = "description version 3",
				},
			},
		},
	}

	rxTest:waitFor( memberCatalogSpec:put( assetDocPutUrl3, assetDocContent3, assetDocPutArgs ) )


	local assetDocRev4Id = KSIdGenerator.uuidInLowercaseHex()

	local assetDocPutUrl4 = string.format( '/v2/spaces/%s/assets/%s/revisions/%s/children/%s',
											spaceId,
											assetId,
											assetDocRev3Id,
											assetDocRev4Id )

	local assetDocContent4 = KSJsonEncoder.encodeOzUploadJson {
		user_id = memberCatalogSpec:wfOzUser():userId(),
		payload = {
			xmp = {
				dc = {
					description = "description version 4",
				},
			},
		},
	}

	rxTest:waitFor( memberCatalogSpec:put( assetDocPutUrl4, assetDocContent4, assetDocPutArgs ) )
	
	-- Now check asset document from Oz

	rxTest:expectSingleValue( 
		memberCatalogSpec:get( assetDocGetUrl, assetDocGetArgs )
			:map( function( data, header )
				
				local assetDoc = KSJsonDecoder.decodeWebSafeJson_stripNull( data )
				
				local revision_ids = KSTableUtils.derefOpt( assetDoc, 'revision_ids' )
				
				return #revision_ids == 5
					and KSTableUtils.derefOpt( assetDoc, 'payload', 'xmp', 'dc', 'description' ) == "description version 4"
				
			end ),
		true )
	
	-- Switch to owner catalog
	
	rxTest:expectOnlyCompleted( ownerCatalogTestSession:reloadAccessAndRefreshTokens() )
	
	-- Make one more change remote from owner

	local assetDocRev5Id = KSIdGenerator.uuidInLowercaseHex()

	local assetDocPutUrl5 = string.format( '%s/assets/%s/revisions/%s/children/%s',
										   ownerCatalogSpec:catalogHref(),
										   assetId,
										   assetDocRev4Id,
										   assetDocRev5Id )

	local assetDocContent5 = KSJsonEncoder.encodeOzUploadJson {
		user_id = ownerCatalogSpec:wfOzUser():userId(),
		payload = {
			xmp = {
				dc = {
					description = "description version 5",
				},
			},
		},
	}

	rxTest:waitFor( ownerCatalogSpec:put( assetDocPutUrl5, assetDocContent5, assetDocPutArgs ) )
			
	-- Force a RevisionCompressed doc for this asset from owner
	
	local accessToken = ownerCatalogTestSession._userTestSession._accessToken
	
	local postParams = {
		requestMethod = 'POST',
		requestHeaders = {
			{ header = 'Authorization', value = 'Bearer ' .. accessToken },
			{ header = 'Content-Type', value = 'application/json' },
		}
	}
	
	local compressUrl = string.format( '%s/assets/%s/compress',
										ownerCatalogSpec:catalogHref(), assetId )
	
	local compressFullUrl = 'https://admin.adbephotos-stage.com' .. compressUrl
	
	rxTest:waitFor( WFRxHttpClient.put(compressFullUrl, '{}', postParams ) )

	rxTest:sleep( 5 )

	-- switch back to member
	
	rxTest:expectOnlyCompleted( memberCatalogTestSession:reloadAccessAndRefreshTokens() )
	
	-- Make sure revisions are compressed

	rxTest:expectSingleValue( 
		memberCatalogSpec:get( assetDocGetUrl, assetDocGetArgs )
			:map( function( data, header )
				
				local assetDoc = KSJsonDecoder.decodeWebSafeJson_stripNull( data )
				
				local revision_ids = KSTableUtils.derefOpt( assetDoc, 'revision_ids' )
				
				return #revision_ids == 2
					and KSTableUtils.derefOpt( assetDoc, 'payload', 'xmp', 'dc', 'description' ) == "description version 5"
				
			end ),
		true )
	
end

--------------------------------------------------------------------------------

function WFCatSpaceFeedsManagerTest.rxTestResyncAssetDoc_EditorToContributor_testNightly( rxTest )

	rxTest:setTimeLimit( 300 )
	
	local _, ownerCatalogSpec, ownerCatalogTestSession,
		memberWFCatalog, _, memberCatalogTestSession,
		spaceId, spaceUserId, _, _ = WFCatOzTestSession.setupSpaceOzTest( rxTest, 'editor', nil, true )
	
	local spaceFeedsManagerArgs = {
		taskDelays = {
			progressiveBackoff = false,
			noWorkDelay = 0.5,
			maxNoWorkDelay = 1,
			errorDelay = 1,
			maxDelay = 1,
		},
		isPrefetchEnabled = true,
		albumAssetsForFilteredAssetsQueryFn = albumAssetsForFilteredAssetsQueryFn,
		testMode = true,
	}
	
	local spaceFeedsManager = WFCatSpaceFeedsManager( memberWFCatalog, spaceFeedsManagerArgs )
	rxTest:addCloseTarget( spaceFeedsManager )
	
	spaceFeedsManager:reportCatalogInitialSyncIsComplete()
	spaceFeedsManager:startFetchingUpdates()
	spaceFeedsManager:startSendingUpdates()
	
	local memberAssetId
	
	local querySpaceAssetIdStr
	if WFPlatformInfo.isDesktop then
		querySpaceAssetIdStr = [[SELECT album_asset_v2.assetId 
								FROM albums INNER JOIN album_asset_v2 ON albums.albumId == album_asset_v2.albumId
								WHERE spaceId == ?]]
	else
		querySpaceAssetIdStr = [[SELECT album_asset.assetId
								FROM albums INNER JOIN album_asset ON albums.docId == album_asset.albumId 
								WHERE spaceId == ?]]
	end
	
	rxTest:observe( 
		memberWFCatalog:indexStore():query{
				sql = querySpaceAssetIdStr, spaceId,
				singleColumnQuery = true
			}
			:map( function( assetIds )
				memberAssetId = assetIds[ 1 ]
				return #assetIds == 1
			end ) )
		:expectDataWithinTime( 90, true )

	-- Check if assetDoc is editable
	
	rxTest:expectSingleValue( memberWFCatalog:docStore():docValue( memberAssetId )
		:map( function( assetDoc )
			return WFCatSharedWithYouUtils.canEditAsset( assetDoc )
		end ),
		true
	)
	
	-- Owner Update spaceUser to contributor

	spaceFeedsManager:stopFetchingUpdates()
	spaceFeedsManager:stopSendingUpdates()

	rxTest:expectOnlyCompleted( ownerCatalogTestSession:reloadAccessAndRefreshTokens() )

	local updateSpaceUserPostUrl = string.format( '%s/spaces/%s/users/%s',
											ownerCatalogSpec:catalogHref(),
										 	spaceId,
										 	spaceUserId )

	local updateSpaceUserContent = KSJsonEncoder.encodeOzUploadJson {
		payload = {
			role = 'contributor',
			userUpdated = KSDate.timeInIso8601Format()
		},
	}

	local updateSpaceUserArgs = {
		requestMethod = 'POST',
		returnHttpErrorsAsData = true,
		requestHeaders = {
			{ header = 'Content-Type', value = 'application/json' },
			{ header = 'Accept', value = 'application/json' },
		},
	}

	rxTest:waitFor( ownerCatalogSpec:put( updateSpaceUserPostUrl, updateSpaceUserContent, updateSpaceUserArgs ) )

	-- Swithc back to member. Pull space feed
	
	rxTest:expectOnlyCompleted( memberCatalogTestSession:reloadAccessAndRefreshTokens() )
	
	spaceFeedsManager:startFetchingUpdates()
	spaceFeedsManager:startSendingUpdates()
	
	-- Check if assetDoc is redacted and not editable

    rxTest:observe( 
		memberWFCatalog:docStore():ongoingDocValue( memberAssetId )
			:map( function( docValue )
				
				local editable = KSTableUtils.derefOpt( docValue, '_serverProvidedMetadata', 'editable' )
				local originalLink = KSTableUtils.derefOpt( docValue, '_serverProvidedMetadata', 'links', '/rels/master', 'href' )
				
				return editable == nil and originalLink == nil
				
			end ) )
		:expectDataWithinTime( 90, true )

end

--------------------------------------------------------------------------------

function WFCatSpaceFeedsManagerTest.rxTestRetryAndBlockSpaceFeed( rxTest )

	rxTest:setTimeLimit( 10 )
	
	local expectedResponses = { 
		{
			url = string.format( "/v2/spaces/%s/updates", spaceId1 ),
			
			fail = {
				[[ {
					"errors": {
						"space": [
							"BAR"
						]
					}
				} ]],
				{
					status_code = '504',
				}
			},

			options = {
				bypassRateLimiter = true,
			}
		},
		{
			url = string.format( "/v2/spaces/%s/updates", spaceId1 ),
			
			fail = {
				[[ {
					"errors": {
						"space": [
							"FOO"
						]
					}
				} ]],
				{
					status_code = '504',
				}
			},

			options = {
				bypassRateLimiter = true,
			}
		},
		{
			url = string.format( "/v2/spaces/%s/updates", spaceId1 ),
			
			fail = {
				[[ {
					"errors": {
						"space": [
							"BLAH"
						]
					}
				} ]],
				{
					status_code = '403',
				}
			},

			options = {
				bypassRateLimiter = true,
			}
		}
 
 	}
	
	local args = {
		spacesAreOverQuota = false,
		entitlementExpired = false,
	}
	
	local catalogSpec = createCatalogSpecForTest( rxTest, space1, nil, expectedResponses, args )
	
	local wfCatalog = createWFCatalogForTestWithSpec( rxTest, catalogSpec )
	
	local spaceFeedsManagerArgs = {
		taskDelays = {
			progressiveBackoff = false,
			noWorkDelay = 0.5,
			maxNoWorkDelay = 1,
			errorDelay = 1,
			maxDelay = 1,
		},
		isPrefetchEnabled = true,
		albumAssetsForFilteredAssetsQueryFn = albumAssetsForFilteredAssetsQueryFn,
		testMode = true,
	}

	local spaceFeedsManager = WFCatSpaceFeedsManager( wfCatalog, spaceFeedsManagerArgs )
	rxTest:addCloseTarget( spaceFeedsManager )
	
	rxTest:expectOnlyCompleted( spaceFeedsManager:_testOnly_getUpdatesOnce() )
	
	rxTest:sleep( 3 )
		-- FIXME [jandrew 2019-12-04]: Need to figure out how to get rid of this call to sleep.
	 
end

--------------------------------------------------------------------------------

function WFCatSpaceFeedsManagerTest.rxTestSpaceFeedsMaintenanceNotificationStream( rxTest )

	-- The following test case verifies the maintenanceLock field provided by Oz
	-- on space documents when a space is not accessible due to account maintenance
	-- or an oz outage.

	rxTest:setTimeLimit( 10 )
	
	local expectedResponses = {
		{
		
			url = "/v2/spaces/updates?updated_since=pull1",
			
			response = [[{
			  "base": "https://photos-stage.adobe.io/v2/",
			  "updates": [
				{
				  "created": "2019-08-21T17:01:02.467393Z",
				  "type": "space",
				  "subtype": "shared",
				  "id": "<space1_spaceId>",
				  "owner": {
					"id": "<space1_spaceOwnerId>",
					"outOfQuota": false,
					"entitlementExpired": false,
					"maintenanceLock": false,
					"links": {
					  "self": {
						"href": "spaces/<space1_spaceId>/updates/<space1_spaceOwnerId>"
					  }
					}
				  },
				  "this_space_user": {
					"id": "<space1_youSpaceUserId>",
					"user": {
					  "id": "<space1_youFeedbackUserId>",
					  "links": {
						"self": {
						  "href": "spaces/<space1_spaceId>/updates/<space1_youFeedbackUserId>"
						}
					  }
					},
					"links": {
					  "self": {
						"href": "spaces/<space1_spaceId>/updates/<space1_youSpaceUserId>"
					  }
					}
				  },
				  "albums": [
					{
					  "id": "<album1_id>",
					  "links": {
						"self": {
						  "href": "spaces/<space1_spaceId>/updates/<album1_id>"
						}
					  },
					  "space_album": {
						"id": "<spaceAlbum1_id>",
						"links": {
						  "self": {
							"href": "spaces/<space1_spaceId>/updates/<spaceAlbum1_id>"
						  }
						}
					  }
					}
				  ],
				  "revision_ids": [
					"33ca44a7da51f56e6d3f9e80b9181aaf",
					"813254e958ea3ffac99d32cda7acc969",
					"b6088deb146acbbde9268c50156fa19a",
					"a0cb906593fa65713d198cec5407426a",
					"3a7c6f13acb6fd7afa9d979681ae1e20",
					"8f8b861e17b67454062ab16b7437b189",
					"2ddfea16ac1a4af1cd0b9a1937fc9c3a",
					"d86b4baa17e22e23bf1a87e84e7d9807"
				  ],
				  "updated": "2019-08-21T17:01:08.471614Z",
				  "country_blocked": false,
				  "payload": {
					"userUpdated": "2019-08-21T17:01:08.445000Z",
					"allowInviteRequests": true,
					"userCreated": "2019-08-21T17:01:02.439000Z",
					"presentationFilters": true,
					"location": true,
					"download": true,
					"private": true,
					"metadata": true
				  },
				  "createdOnClient": "Chrome-76.0.3809-MacOSX",
				  "links": {
					"self": {
					  "href": "spaces/<space1_spaceId>/updates/<space1_spaceId>"
					},
					"current": {
					  "href": "spaces/<space1_spaceId>/updates?updated_since=pull1"
					},
					"/rels/web_space": {
					  "href": "http://bit.ly/2ZkCLDo"
					},
					"start": {
					  "href": "spaces/<space1_spaceId>/updates"
					}
				  }
				}
			  ],
			  "links": {
				"next": {
				  "href": "/v2/spaces/updates?updated_since=pull1"
				}
			  }
			}]],
			
			responseFilter = function( response )

				response = response:gsub( '<space1_spaceId>', space1.id )
				response = response:gsub( '<space1_youSpaceUserId>', space1.youSpaceUserId )
				response = response:gsub( '<space1_spaceOwnerId>', space1.ownerId or KSIdGenerator.uuidInLowercaseHex() )
				response = response:gsub( '<space1_youFeedbackUserId>', space1.youFeedbackUserId or KSIdGenerator.uuidInLowercaseHex() ) 
				
				response = response:gsub( '<album1_id>', space1.albumId )
				response = response:gsub( '<spaceAlbum1_id>', space1.spaceAlbumId )

				return response

			end,
			
			headers = {
				status_code = '200',
			},
			
			options = {
				bypassRateLimiter = true,
			}
				
		},
		{
			url = string.format( "/v2/spaces/%s/updates", spaceId1 ),

			response = string.format( [[{
				"base": "https://photos.adobe.io",
				"updates": [],
				"links": {
					"next": {
						"href": "/v2/spaces/%s/updates?updated_since=pull1"
					}
				}
			}]], spaceId1 ),

			headers = {
				status_code = '200',
			},

			options = {
				bypassRateLimiter = true,
			}

		},
		{
			url = string.format( "/v2/spaces/%s/updates?updated_since=pull1", spaceId1 ),

			response = string.format( [[{
				"base": "https://photos.adobe.io",
				"updates": [],
				"links": {
					"next": {
						"href": "/v2/spaces/%s/updates?updated_since=pull1"
					}
				}
			}]], spaceId1 ),

			headers = {
				status_code = '200',
			},

			options = {
				bypassRateLimiter = true,
			}

		}
		
 	}
	
	local args = {
		spacesAreOverQuota = false,
		entitlementExpired = false,
		maintenanceLock = true,
	}
	
	local catalogSpec = createCatalogSpecForTest( rxTest, space1, nil, expectedResponses, args )
	
	local wfCatalog = createWFCatalogForTestWithSpec( rxTest, catalogSpec )
	
	local spaceFeedsManagerArgs = {
		taskDelays = {
			progressiveBackoff = false,
			noWorkDelay = 0.5,
			maxNoWorkDelay = 1,
			errorDelay = 1,
			maxDelay = 1,
		},
		isPrefetchEnabled = true,
		albumAssetsForFilteredAssetsQueryFn = albumAssetsForFilteredAssetsQueryFn,
		testMode = true,
	}

	-- Create the Space Feeds Manager

	local spaceFeedsManager = WFCatSpaceFeedsManager( wfCatalog, spaceFeedsManagerArgs )
	rxTest:addCloseTarget( spaceFeedsManager )
	
	local spaceFeedsMaintenanceObserver = rxTest:observe( spaceFeedsManager:spaceFeedsMaintenanceNotificationStream() )
	
	rxTest:expectOnlyCompleted( spaceFeedsManager:_testOnly_getUpdatesOnce() )
	
	-- Make sure that we now have the required local documents. These documents
	-- are required even when the account is in maintenance mode.
	
	local observer = rxTest:observe( wfCatalog:indexStore():query( {
			sql = [[ SELECT COUNT( * ) FROM feedback_user ]],
		} ):map( function( rows ) return rows[ 1 ][ 1 ] end ) )
			-- This checks for the feedback user docs
			
	observer:expectDataWithinTime( 2, 2 )
	
	-- Check for Albums / Space Albums
	
	rxTest:expectSingleValue( wfCatalog:docStore():docValue( albumId1 )
		:map( function( docValue )
			return not not docValue._empty
		end ), false )
		
	rxTest:expectSingleValue( wfCatalog:docStore():docValue( spaceAlbumId1 )
		:map( function( docValue )
			return not not docValue._empty
		end ), false )

	-- Verify that the space is in maintenance mode.

	spaceFeedsMaintenanceObserver:expectDataWithinTime( 2, 
		{
			id = spaceId1, 
			isUnderMaintenance = true
		} )
		
	-- Get more updates. This should remove the space from maintenance mode.
	
	local isDownloadingObserver = rxTest:observe( spaceFeedsManager:isDownloadingMetadata() )
	
	local activeSpacesObserver = rxTest:observe( spaceFeedsManager._activeSpacesStream )
	
	rxTest:expectOnlyCompleted( spaceFeedsManager:_testOnly_getUpdatesOnce() )
	
	activeSpacesObserver:expectDataWithinTime( 2, spaceId1, spaceUserId1, ":ignore:", ":ignore:", ":ignore:", ":ignore:" )
	
	activeSpacesObserver:dispose()

	-- Verify that the space is no longer in maintenance mode.
	
	spaceFeedsMaintenanceObserver:expectDataWithinTime( 2, 
		{
			id = spaceId1, 
			isUnderMaintenance = false
		} )
	
	spaceFeedsMaintenanceObserver:dispose()

	-- The puller associated with the space should now become active since the
	-- space is no longer in maintenance mode, and then inactive.

	isDownloadingObserver:expectDataWithinTime( 2, true )
	
	isDownloadingObserver:expectDataWithinTime( 2, false )
	
	isDownloadingObserver:dispose()	

end

--------------------------------------------------------------------------------

return WFRxTestCase.wrapSuite( WFCatSpaceFeedsManagerTest )
