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

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 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 verifyLogData( rxTest, actualLogData, expectedLogData, expectedUpdateFeedLink, maybeSpaceUserId )

	local nextUpdateFeedLink = actualLogData.nextUpdateFeedLink
	actualLogData.nextUpdateFeedLink = nil
	actualLogData.isOzCatalogDeleted = nil
	actualLogData.lastUpdateCycleWithData = nil
	actualLogData.isCurrentlySyncing = nil

	-- for end to end tests can be difficult to be sure how many batches data will come in
	if expectedLogData.successfulUpdateCycles == '<number>' then
		actualLogData.successfulUpdateCycles = '<number>'
	end

	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 spaceFeedArgs = { isPrefetchEnabled = true }
	local spaceFeedsManager = WFCatOzTestSession.createSpaceFeedsManager( rxTest, wfCatalog, spaceFeedArgs )

	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 )

	-- 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 = WFCatOzTestSession.createSpaceFeedsManager( rxTest, wfCatalog )

	-- 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 spaceFeedsManager = WFCatOzTestSession.createSpaceFeedsManager( rxTest, wfCatalog )

	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() )

	canFetchUpdatesStream:expectData( false )
	canSendUpdatesStream:expectData( false )

	-- Initial sync is not complete, so there should be no space feed manager activity.

	spaceFeedsManager:reportCatalogInitialSyncIsComplete( false )

	canFetchUpdatesStream:expectData( false )
	canSendUpdatesStream:expectData( false )

	spaceFeedsManager:startFetchingUpdates()

	canFetchUpdatesStream:expectData( false )
	canSendUpdatesStream:expectData( false )

	spaceFeedsManager:startSendingUpdates()

	canFetchUpdatesStream:expectData( false )
	canSendUpdatesStream:expectData( false )

	spaceFeedsManager:stopFetchingUpdates()

	canFetchUpdatesStream:expectData( false )
	canSendUpdatesStream:expectData( false )

	spaceFeedsManager:stopSendingUpdates()

	canFetchUpdatesStream:expectData( false )
	canSendUpdatesStream:expectData( false )

	-- Initial sync is complete, so there should be space feed manager activity.

	spaceFeedsManager:reportCatalogInitialSyncIsComplete( true )

	canFetchUpdatesStream:expectData( false )
	canSendUpdatesStream:expectData( false )

	spaceFeedsManager:startFetchingUpdates()

	canFetchUpdatesStream:expectData( true )
	canSendUpdatesStream:expectData( false )

	spaceFeedsManager:startSendingUpdates()

	canFetchUpdatesStream:expectDataWithinTime( true )
	canSendUpdatesStream:expectDataWithinTime( true )

	spaceFeedsManager:stopSendingUpdates()

	canFetchUpdatesStream:expectDataWithinTime( true )
	canSendUpdatesStream:expectDataWithinTime( false )

	spaceFeedsManager:stopFetchingUpdates()

	canFetchUpdatesStream:expectDataWithinTime( false )
	canSendUpdatesStream:expectDataWithinTime( 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 spaceFeedArgs = { isPrefetchEnabled = true }
	local spaceFeedsManager = WFCatOzTestSession.createSpaceFeedsManager( rxTest, wfCatalog, spaceFeedArgs )

	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


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

-- FIXME [jandrew 2025-03-11]: Disabled due to instability.

function WFCatSpaceFeedsManagerTest.DISABLED_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 spaceFeedArgs = { isPrefetchEnabled = true }
	local spaceFeedsManager = WFCatOzTestSession.createSpaceFeedsManager( rxTest, wfCatalog, spaceFeedArgs )

	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:waitFor( wfCatalog:docStore():setDocValue( assetDoc ) )

	-- Trigger the space metadata pusher.

	local sendNUpdates = rxTest:observe( spaceFeedsManager._spacePusher:_DEBUG_sendUpdatesOnce() )
	sendNUpdates:expectCompletedWithinTime( 5 )
	sendNUpdates:dispose()

	-- Verify the asset doc is marked with TrialOrSubscriptionExpiredError

	rxTest:expectSingleValue(

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

	if WFPlatformInfo.isNimbus() then

		-- The following is LrD-only, as these specific types of binary download
		-- errors are only annotated by the pixel provider.

		-- Add another asset to the space. Make it look like this asset encountered
		-- a binary download error from trying to download  it from an expired
		-- space.

		local assetDocId2 = KSIdGenerator.uuidInLowercaseHex()
		local assetDoc2 = buildDocValue( rxTest, wfCatalog, assetDocId2 )
		assetDoc2._spaceId = spaceId1
		
		local originalHref = KSTableUtils.derefOpt( assetDoc2, '_serverProvidedMetadata', 'links', '/rels/master', 'href' )

		KSTableUtils.setNested(
				assetDoc2, true, '_localOnly', 'binaryDownloadErrors', '/rels/master', originalHref )

		rxTest:waitFor( wfCatalog:docStore():setDocValue( assetDoc2 ) )

		-- Trigger the code that should reset the binary download error. Note we're
		-- doing this manually, because normally it's triggered when the space puller
		-- is set up. And in this particular test case, the space puller isn't initialized.

		spaceFeedsManager:_testOnly_resetEntitlementExpiredErrorsForSpaceTask( spaceId1, space1.youSpaceUserId )

		-- Verify the download error was cleared.

		local hasBinaryDownloadErrorsStream = rxTest:observe( wfCatalog:docStore():ongoingDocValue( assetDocId2 )
				:map( function( docValue )
					return not not KSTableUtils.derefOpt( docValue, '_localOnly', 'binaryDownloadErrors' )
				end ) )

		hasBinaryDownloadErrorsStream:expectDataWithinTime( 5, false )
		
		hasBinaryDownloadErrorsStream:dispose()

	end

	-- 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":{"importedBy":"9cb9fb43d26c4691a9fd32e633434796","fileSize":45,,"importTimestamp":"2023-12-20T17:19:16.448Z"},"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 )

	-- Add an asset to the space.

	local assetDoc = buildDocValue( rxTest, wfCatalog, assetDocId, { noXmp = true } )
	assetDoc._spaceId = spaceId1
	assetDoc._newRevId = newRevId1
	assetDoc.develop = nil
	KSTableUtils.setNested( assetDoc, nil, 'importSource', 'fileName' )
	KSTableUtils.setNested( assetDoc, captureDate, 'captureDate' )

	rxTest:expectSingleValue( wfCatalog:docStore():setDocValue( assetDoc ), ':ignore:' )

	-- Start up the space feed manager

	local spaceFeedsManager = WFCatOzTestSession.createSpaceFeedsManager( rxTest, wfCatalog )

	-- 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":{"importedBy":"9cb9fb43d26c4691a9fd32e633434796","fileSize":45,,"importTimestamp":"2023-12-20T17:19:16.448Z"},"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 )

	-- Add an asset to the space.

	local assetDoc = buildDocValue( rxTest, wfCatalog, assetDocId, { noXmp = true } )
	assetDoc._spaceId = spaceId1
	assetDoc._newRevId = newRevId1
	assetDoc.develop = nil
	KSTableUtils.setNested( assetDoc, nil, 'importSource', 'fileName' )
	KSTableUtils.setNested( assetDoc, captureDate, 'captureDate' )

	rxTest:expectSingleValue( wfCatalog:docStore():setDocValue( assetDoc ), ':ignore:' )

	-- Start up the space feed manager

	local spaceFeedsManager = WFCatOzTestSession.createSpaceFeedsManager( rxTest, wfCatalog )

	-- 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":{"importedBy":"9cb9fb43d26c4691a9fd32e633434796","fileSize":45,,"importTimestamp":"2023-12-20T17:19:16.448Z"},"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 )

	-- Add an asset to the space.

	local assetDoc = buildDocValue( rxTest, wfCatalog, assetDocId, { noXmp = true } )
	assetDoc._spaceId = spaceId1
	assetDoc._newRevId = newRevId1
	assetDoc.develop = nil
	KSTableUtils.setNested( assetDoc, nil, 'importSource', 'fileName' )
	KSTableUtils.setNested( assetDoc, captureDate, 'captureDate' )

	rxTest:expectSingleValue( wfCatalog:docStore():setDocValue( assetDoc ), ':ignore:' )

	-- Start up the space feed manager

	local spaceFeedsManager = WFCatOzTestSession.createSpaceFeedsManager( rxTest, wfCatalog )

	-- 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":{"importedBy":"9cb9fb43d26c4691a9fd32e633434796","fileSize":45,,"importTimestamp":"2023-12-20T17:19:16.448Z"},"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 )

	-- Add an asset to the space.

	local assetDoc = buildDocValue( rxTest, wfCatalog, assetDocId, { noXmp = true } )
	assetDoc._spaceId = spaceId1
	assetDoc._newRevId = newRevId1
	assetDoc.develop = nil
	KSTableUtils.setNested( assetDoc, nil, 'importSource', 'fileName' )
	KSTableUtils.setNested( assetDoc, captureDate, 'captureDate' )

	rxTest:expectSingleValue( wfCatalog:docStore():setDocValue( assetDoc ), ':ignore:' )

	-- Start up the space feed manager

	local spaceFeedsManager = WFCatOzTestSession.createSpaceFeedsManager( rxTest, wfCatalog )

	-- 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 spaceFeedsManager = WFCatOzTestSession.createSpaceFeedsManager( rxTest, wfCatalog )

	-- 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:waitFor( 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 spaceFeedArgs = { isPrefetchEnabled = true }
	local spaceFeedsManager = WFCatOzTestSession.createSpaceFeedsManager( rxTest, wfCatalog, spaceFeedArgs )

	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 )

	-- Start up the space feed manager

	local spaceFeedArgs = { isPrefetchEnabled = true }
	local spaceFeedsManager = WFCatOzTestSession.createSpaceFeedsManager( rxTest, wfCatalog, spaceFeedArgs )

	-- 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 )

	-- Start up the space feed manager

	local spaceFeedArgs = { isPrefetchEnabled = true }
	local spaceFeedsManager = WFCatOzTestSession.createSpaceFeedsManager( rxTest, wfCatalog, spaceFeedArgs )

	-- 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":{"importedBy":"9cb9fb43d26c4691a9fd32e633434796","fileSize":45,,"importTimestamp":"2023-12-20T17:19:16.448Z"},"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 )

	-- Start up the space feed manager

	local spaceFeedArgs = { isPrefetchEnabled = true }
	local spaceFeedsManager = WFCatOzTestSession.createSpaceFeedsManager( rxTest, wfCatalog, spaceFeedArgs )

	-- 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, { noXmp = true } )
	assetDoc._spaceId = spaceId1
	assetDoc._newRevId = newRevId1
	assetDoc.develop = nil
	KSTableUtils.setNested( assetDoc, nil, 'importSource', 'fileName' )
	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 )

	-- Start up the space feed manager

	local spaceFeedsManager = WFCatOzTestSession.createSpaceFeedsManager( rxTest, wfCatalog )

	-- 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 )

	-- Start up the space feed manager

	local spaceFeedsManager = WFCatOzTestSession.createSpaceFeedsManager( rxTest, wfCatalog )

	-- 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 = WFCatOzTestSession.createSpaceFeedsManager( rxTest, wfCatalog )

	-- 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 )

	-- Start up the space feed manager

	local spaceFeedsManager = WFCatOzTestSession.createSpaceFeedsManager( rxTest, wfCatalog )

	-- 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 spaceFeedArgs = { isPrefetchEnabled = true }
	local spaceFeedsManager = WFCatOzTestSession.createSpaceFeedsManager( rxTest, memberWFCatalog, spaceFeedArgs )

	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, _, _, spaceId, spaceUserId = WFCatOzTestSession.setupSpaceOzTest( rxTest, 'viewer', nil, true )

	local spaceFeedArgs = { isPrefetchEnabled = true }
	local spaceFeedsManager = WFCatOzTestSession.createSpaceFeedsManager( rxTest, memberWFCatalog, spaceFeedArgs )

	local observer = rxTest:observe( spaceFeedsManager:isDownloadingMetadata() )

	spaceFeedsManager:reportCatalogInitialSyncIsComplete()
	spaceFeedsManager:startFetchingUpdates()
	spaceFeedsManager:startSendingUpdates()

	observer:expectDataWithinTime( 60, true )
	observer:expectDataWithinTime( 10, false )

	observer:dispose()

	-- wait for album, album_asset and asset to download

	local sql
	if WFPlatformInfo.isMobile() then
		sql = [[ SELECT docId FROM albums WHERE spaceId == ? ]]
	else
		sql = [[ SELECT albumId FROM albums WHERE spaceId == ? ]]
	end
	local albumId = rxTest:getFirstValue(
		memberWFCatalog:indexStore(): query( {
			sql = sql,
			spaceId,
			singleColumnQuery = true }
		)
		:filter( function( albumIds )
			return #albumIds > 0
		end )
		:pluck( 1 )
		:take( 1 ) )

	if WFPlatformInfo.isMobile() then
		sql = [[ SELECT assetId FROM album_asset WHERE albumId = ?]]
	else
		sql = [[ SELECT assetId FROM album_asset_v2 WHERE albumId = ? ]]
	end
	local assetId = rxTest:getFirstValue(
		memberWFCatalog:indexStore(): query( {
			sql = sql,
			albumId,
			singleColumnQuery = true }
		)
		:filter( function( assetIds )
			return #assetIds > 0
		end )
		:pluck( 1 )
		:take( 1 ) )

	local assetObserver = rxTest:observe( memberWFCatalog:docStore():ongoingDocValue( assetId )
		:map( function( docValue )
			return not not docValue
		end ) )
	assetObserver:expectDataWithinTime( 60, true )

	-- check sync logs

	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 = '<number>',
		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 spaceFeedArgs = { isPrefetchEnabled = true }
	local spaceFeedsManager = WFCatOzTestSession.createSpaceFeedsManager( rxTest, memberWFCatalog, spaceFeedArgs )

	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 spaceFeedsManager = WFCatOzTestSession.createSpaceFeedsManager( rxTest, memberWFCatalog )

	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 spaceFeedArgs = { isPrefetchEnabled = true }
	local spaceFeedsManager = WFCatOzTestSession.createSpaceFeedsManager( rxTest, wfCatalog, spaceFeedArgs )

	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 spaceFeedArgs = { isPrefetchEnabled = true }
	local spaceFeedsManager = WFCatOzTestSession.createSpaceFeedsManager( rxTest, wfCatalog, spaceFeedArgs )

	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

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

function WFCatSpaceFeedsManagerTest.rxTestPrioritizeSpaceSync( 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,
			}

		},
		
		-- puller2 won't pull spaceId2 since we are prioritize Space1 now 
 	}

	local args = {
		spacesAreOverQuota = false,
		entitlementExpired = false,
	}

	local catalogSpec = createCatalogSpecForTest( rxTest, space1, space2, expectedResponses, args )

	local wfCatalog = createWFCatalogForTestWithSpec( rxTest, catalogSpec )

	local spaceFeedArgs = { isPrefetchEnabled = true }
	local spaceFeedsManager = WFCatOzTestSession.createSpaceFeedsManager( rxTest, wfCatalog, spaceFeedArgs )

	local observer = rxTest:observe( spaceFeedsManager:isDownloadingMetadata() )
	
	spaceFeedsManager:prioritizeSpaceSync( spaceId1 )

	rxTest:expectOnlyCompleted( spaceFeedsManager:_testOnly_getUpdatesOnce() )

	observer:expectDataWithinTime( 2, true ) -- Space 1
	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 puller2 = spaceFeedsManager._spacePullers[ spaceUserId2 ]
	rxTest:expectNotEqual( puller, nil )
	
	-- Check that _prioritizedSpaceId is set to spaceId1 
	rxTest:expectEqual( spaceFeedsManager._prioritizedSpaceId, spaceId1 )
	
	-- After prioritization, clean the prioritize space
	spaceFeedsManager:stopPrioritizeSpaceSync()
	rxTest:expectEqual( spaceFeedsManager._prioritizedSpaceId, false )

end

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

return WFRxTestCase.wrapSuite( 'WFCatSpaceFeedsManagerTest', WFCatSpaceFeedsManagerTest )
