Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix new created channels miss initial values #945

Merged
merged 2 commits into from
Mar 31, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -331,10 +331,11 @@ public class _ChatChannelController<ExtraData: ExtraDataTypes>: DataController,
self.callback { completion?(error) }
}

/// Setup event observers if channel is already created on backend side and have a valid `cid`.
/// Otherwise they will be set up after channel creation.
if let cid = cid, isChannelAlreadyCreated {
/// Setup observers if we know the channel `cid` (if it's missing, it'll be set in `set(cid:)`
/// Otherwise they will be set up after channel creation, in `set(cid:)`.
if let cid = cid {
setupEventObservers(for: cid)
setLocalStateBasedOnError(startDatabaseObservers())
}
}

Expand All @@ -345,16 +346,34 @@ public class _ChatChannelController<ExtraData: ExtraDataTypes>: DataController,
/// If the newly created channel has a different cid than initially thought
/// (such is the case for direct messages - backend generates custom cid),
/// this function will set the new cid and reset observers.
/// If the cid is still the same, this function will only reset the observers
/// - since we don't need to set a new query in that case.
/// If the cid is not changed, this function will not do anything.
/// - Parameter cid: New cid for the channel
/// - Returns: Error if it occurs while setting up database observers.
private func set(cid: ChannelId) -> Error? {
if channelQuery.cid != cid {
channelQuery = _ChannelQuery(cid: cid, channelQuery: channelQuery)
}
guard self.cid != cid else { return nil }

channelQuery = _ChannelQuery(cid: cid, channelQuery: channelQuery)
setupEventObservers(for: cid)
return startDatabaseObservers()

let error = startDatabaseObservers()
guard error == nil else { return error }

// If there's a channel already in the database, we must
// simulate the existing data callbacks.
// Otherwise, the changes will be reported when DB write is completed.

// The reason is, when we don't have the cid, the initial fetches return empty/nil entities
// and only following updates are reported, hence initial values are ignored.
guard let channel = channel else { return nil }
delegateCallback {
$0.channelController(self, didUpdateChannel: .create(channel))
$0.channelController(
self,
didUpdateMessages: self.messages.enumerated()
.map { ListChange.insert($1, index: IndexPath(item: $0, section: 0)) }
)
}
return nil
}

private func startDatabaseObservers() -> Error? {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,81 @@ class ChannelController_Tests: StressTestCase {
super.tearDown()
}

// MARK: - Helpers

func setupControllerForNewDirectMessageChannel(currentUserId: UserId, otherUserId: UserId) {
let payload = ChannelEditDetailPayload<NoExtraData>(
type: .messaging,
name: nil,
imageURL: nil,
team: nil,
members: [currentUserId, otherUserId],
invites: [],
extraData: .defaultValue
)

controller = ChatChannelController(
channelQuery: .init(channelPayload: payload),
client: client,
environment: env.environment,
isChannelAlreadyCreated: false
)
controller.callbackQueue = .testQueue(withId: controllerCallbackQueueID)
}

func setupControllerForNewChannel(query: ChannelQuery) {
controller = ChatChannelController(
channelQuery: query,
client: client,
environment: env.environment,
isChannelAlreadyCreated: false
)
controller.callbackQueue = .testQueue(withId: controllerCallbackQueueID)
controller.synchronize()
}

func setupControllerForNewMessageChannel(cid: ChannelId) {
let payload = ChannelEditDetailPayload<NoExtraData>(
cid: cid,
name: nil,
imageURL: nil,
team: nil,
members: [],
invites: [],
extraData: .defaultValue
)

controller = ChatChannelController(
channelQuery: .init(channelPayload: payload),
client: client,
environment: env.environment,
isChannelAlreadyCreated: false
)
controller.callbackQueue = .testQueue(withId: controllerCallbackQueueID)
}

// Helper function that creates channel with message
func setupChannelWithMessage(_ session: DatabaseSession) throws -> MessageId {
let dummyUserPayload: CurrentUserPayload<NoExtraData> = .dummy(userId: .unique, role: .user)
try session.saveCurrentUser(payload: dummyUserPayload)
try session.saveChannel(payload: dummyPayload(with: channelId))
let message = try session.createNewMessage(
in: channelId,
text: "Message",
pinning: nil,
quotedMessageId: nil,
attachmentSeeds: [
ChatMessageAttachmentSeed.dummy(),
ChatMessageAttachmentSeed.dummy(),
ChatMessageAttachmentSeed.dummy()
],
extraData: NoExtraData.defaultValue
)
return message.id
}

// MARK: - Init tests

func test_clientAndIdAreCorrect() {
XCTAssert(controller.client === client)
XCTAssertEqual(controller.channelQuery.cid, channelId)
Expand Down Expand Up @@ -788,17 +863,145 @@ class ChannelController_Tests: StressTestCase {
}
}

// MARK: - Channel actions propagation tests

func setupControllerForNewChannel(query: ChannelQuery) {
controller = ChatChannelController(
channelQuery: query,
client: client,
environment: env.environment,
isChannelAlreadyCreated: false
// MARK: - New direct message channel creation tests

func test_controller_reportsInitialValues_forDMChannel_ifChannelDoesntExistLocally() throws {
// Create mock users
let currentUserId = UserId.unique
let otherUserId = UserId.unique

// Create controller for the non-existent new DM channel
setupControllerForNewDirectMessageChannel(currentUserId: currentUserId, otherUserId: otherUserId)

// Create and set delegate
let delegate = TestDelegate(expectedQueueId: controllerCallbackQueueID)
controller.delegate = delegate

// Simulate synchronize
controller.synchronize()

// Create dummy channel with messages
let dummyChannel = dummyPayload(
with: .unique,
numberOfMessages: 10,
members: [.dummy(userId: currentUserId), .dummy(userId: otherUserId)]
)
controller.callbackQueue = .testQueue(withId: controllerCallbackQueueID)

// Simulate successful backend channel creation
env.channelUpdater!.update_channelCreatedCallback?(dummyChannel.channel.cid)

// Simulate new channel creation in DB
try client.databaseContainer.writeSynchronously { session in
try session.saveChannel(payload: dummyChannel)
}

// Simulate successful network call
env.channelUpdater!.update_completion?(nil)

// Assert that initial reported values are correct
XCTAssertEqual(controller.channel?.cid, dummyChannel.channel.cid)
XCTAssertEqual(controller.messages.count, dummyChannel.messages.count)
Comment on lines +902 to +903
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shouldn't we assert the delegate calls here, too?


// Assert the delegate is called for initial values
XCTAssertEqual(delegate.didUpdateChannel_channel?.item.cid, dummyChannel.channel.cid)
XCTAssertEqual(delegate.didUpdateMessages_messages?.count, dummyChannel.messages.count)
}

func test_controller_reportsInitialValues_forDMChannel_ifChannelExistsLocally() throws {
// Create mock users
let currentUserId = UserId.unique
let otherUserId = UserId.unique

// Create dummy channel with messages
let dummyChannel = dummyPayload(
with: .unique,
numberOfMessages: 10,
members: [.dummy(userId: currentUserId), .dummy(userId: otherUserId)]
)

// Simulate new channel creation in DB
try client.databaseContainer.writeSynchronously { session in
try session.saveChannel(payload: dummyChannel)
}

// Create controller for the existing new DM channel
setupControllerForNewDirectMessageChannel(currentUserId: currentUserId, otherUserId: otherUserId)

// Create and set delegate
let delegate = TestDelegate(expectedQueueId: controllerCallbackQueueID)
controller.delegate = delegate

// Simulate synchronize
controller.synchronize()

// Simulate successful backend channel creation
env.channelUpdater!.update_channelCreatedCallback?(dummyChannel.channel.cid)

// Simulate successful network call.
env.channelUpdater!.update_completion?(nil)

// Since initially the controller doesn't know it's final `cid`, it can't report correct initial values.
// That's why we simulate delegate callbacks for initial values.
// Assert that delegate gets initial values as callback
AssertAsync {
Assert.willBeEqual(delegate.didUpdateChannel_channel?.item.cid, dummyChannel.channel.cid)
Assert.willBeEqual(delegate.didUpdateMessages_messages?.count, dummyChannel.messages.count)
}
}

// MARK: - New channel creation tests

func test_controller_reportsInitialValues_forNewChannel_ifChannelDoesntExistLocally() throws {
// Create controller for the non-existent new DM channel
setupControllerForNewMessageChannel(cid: channelId)

// Simulate synchronize
controller.synchronize()

// Create dummy channel with messages
let dummyChannel = dummyPayload(
with: channelId,
numberOfMessages: 10,
members: [.dummy()]
)

// Simulate successful backend channel creation
env.channelUpdater!.update_channelCreatedCallback?(dummyChannel.channel.cid)

// Simulate new channel creation in DB
try client.databaseContainer.writeSynchronously { session in
try session.saveChannel(payload: dummyChannel)
}

// Simulate successful network call
env.channelUpdater!.update_completion?(nil)

// Assert that initial reported values are correct
XCTAssertEqual(controller.channel?.cid, dummyChannel.channel.cid)
XCTAssertEqual(controller.messages.count, dummyChannel.messages.count)
}

func test_controller_reportsInitialValues_forNewChannel_ifChannelExistsLocally() throws {
// Create dummy channel with messages
let dummyChannel = dummyPayload(
with: channelId,
numberOfMessages: 10,
members: [.dummy()]
)

// Simulate new channel creation in DB
try client.databaseContainer.writeSynchronously { session in
try session.saveChannel(payload: dummyChannel)
}

// Create controller for the existing new DM channel
setupControllerForNewMessageChannel(cid: channelId)

// Unlike new DM ChannelController, this ChannelController knows it's final `cid` so it should be able to fetch initial values
// from DB, without the `synchronize` call
// Assert that initial reported values are correct
XCTAssertEqual(controller.channel?.cid, dummyChannel.channel.cid)
XCTAssertEqual(controller.messages.count, dummyChannel.messages.count)
}

// MARK: - Updating channel
Expand Down Expand Up @@ -1348,28 +1551,6 @@ class ChannelController_Tests: StressTestCase {
AssertAsync.willBeEqual(completionCalledError as? TestError, testError)
}

// MARK: - Message loading

// Helper function that creates channel with message
func setupChannelWithMessage(_ session: DatabaseSession) throws -> MessageId {
let dummyUserPayload: CurrentUserPayload<NoExtraData> = .dummy(userId: .unique, role: .user)
try session.saveCurrentUser(payload: dummyUserPayload)
try session.saveChannel(payload: dummyPayload(with: channelId))
let message = try session.createNewMessage(
in: channelId,
text: "Message",
pinning: nil,
quotedMessageId: nil,
attachmentSeeds: [
ChatMessageAttachmentSeed.dummy(),
ChatMessageAttachmentSeed.dummy(),
ChatMessageAttachmentSeed.dummy()
],
extraData: NoExtraData.defaultValue
)
return message.id
}

// MARK: - `loadPreviousMessages`

func test_loadPreviousMessages_callsChannelUpdater() throws {
Expand Down