|
1 | | -// ignore_for_file: lines_longer_than_80_chars |
| 1 | +// ignore_for_file: lines_longer_than_80_chars, cascade_invocations |
2 | 2 |
|
3 | 3 | import 'package:mocktail/mocktail.dart'; |
4 | 4 | import 'package:stream_chat/stream_chat.dart'; |
@@ -5095,4 +5095,166 @@ void main() { |
5095 | 5095 | expect(channel.canUpdateChannel, false); |
5096 | 5096 | }); |
5097 | 5097 | }); |
| 5098 | + |
| 5099 | + group('Channel State Validation and Cooldown', () { |
| 5100 | + late final client = MockStreamChatClient(); |
| 5101 | + const channelId = 'test-channel-id'; |
| 5102 | + const channelType = 'test-channel-type'; |
| 5103 | + |
| 5104 | + setUpAll(() { |
| 5105 | + // detached loggers |
| 5106 | + when(() => client.detachedLogger(any())).thenAnswer((invocation) { |
| 5107 | + final name = invocation.positionalArguments.first; |
| 5108 | + return _createLogger(name); |
| 5109 | + }); |
| 5110 | + |
| 5111 | + final retryPolicy = RetryPolicy( |
| 5112 | + shouldRetry: (_, __, ___) => false, |
| 5113 | + delayFactor: Duration.zero, |
| 5114 | + ); |
| 5115 | + when(() => client.retryPolicy).thenReturn(retryPolicy); |
| 5116 | + |
| 5117 | + // fake clientState |
| 5118 | + final clientState = FakeClientState(); |
| 5119 | + when(() => client.state).thenReturn(clientState); |
| 5120 | + |
| 5121 | + // client logger |
| 5122 | + when(() => client.logger).thenReturn(_createLogger('mock-client-logger')); |
| 5123 | + }); |
| 5124 | + |
| 5125 | + group('Non-initialized channel state validation', () { |
| 5126 | + test( |
| 5127 | + 'should throw StateError when accessing cooldown on non-initialized channel', |
| 5128 | + () { |
| 5129 | + final channel = Channel(client, channelType, channelId); |
| 5130 | + expect(() => channel.cooldown, throwsA(isA<StateError>())); |
| 5131 | + }, |
| 5132 | + ); |
| 5133 | + |
| 5134 | + test( |
| 5135 | + 'should throw StateError when accessing getRemainingCooldown on non-initialized channel', |
| 5136 | + () { |
| 5137 | + final channel = Channel(client, channelType, channelId); |
| 5138 | + expect(channel.getRemainingCooldown, throwsA(isA<StateError>())); |
| 5139 | + }, |
| 5140 | + ); |
| 5141 | + |
| 5142 | + test( |
| 5143 | + 'should throw StateError when accessing cooldownStream on non-initialized channel', |
| 5144 | + () { |
| 5145 | + final channel = Channel(client, channelType, channelId); |
| 5146 | + expect(() => channel.cooldownStream, throwsA(isA<StateError>())); |
| 5147 | + }, |
| 5148 | + ); |
| 5149 | + }); |
| 5150 | + |
| 5151 | + group('Initialized channel cooldown functionality', () { |
| 5152 | + late Channel channel; |
| 5153 | + |
| 5154 | + setUp(() { |
| 5155 | + final channelState = _generateChannelState(channelId, channelType); |
| 5156 | + channel = Channel.fromState(client, channelState); |
| 5157 | + }); |
| 5158 | + |
| 5159 | + tearDown(() => channel.dispose()); |
| 5160 | + |
| 5161 | + test( |
| 5162 | + 'should return default cooldown value of 0 for initialized channel', |
| 5163 | + () => expect(channel.cooldown, equals(0)), |
| 5164 | + ); |
| 5165 | + |
| 5166 | + test('should return custom cooldown value when set in channel model', () { |
| 5167 | + final channelWithCooldown = ChannelModel( |
| 5168 | + id: channelId, |
| 5169 | + type: channelType, |
| 5170 | + cooldown: 30, |
| 5171 | + ); |
| 5172 | + |
| 5173 | + final stateWithCooldown = ChannelState(channel: channelWithCooldown); |
| 5174 | + final testChannel = Channel.fromState(client, stateWithCooldown); |
| 5175 | + addTearDown(testChannel.dispose); |
| 5176 | + |
| 5177 | + expect(testChannel.cooldown, equals(30)); |
| 5178 | + }); |
| 5179 | + |
| 5180 | + test('should return 0 remaining cooldown when no cooldown is set', () { |
| 5181 | + expect(channel.getRemainingCooldown(), equals(0)); |
| 5182 | + }); |
| 5183 | + |
| 5184 | + test('should return cooldown stream with default value', () { |
| 5185 | + expectLater(channel.cooldownStream.take(1), emits(0)); |
| 5186 | + }); |
| 5187 | + }); |
| 5188 | + |
| 5189 | + group('Disposed channel state validation', () { |
| 5190 | + late Channel channel; |
| 5191 | + |
| 5192 | + setUp(() { |
| 5193 | + final channelState = _generateChannelState(channelId, channelType); |
| 5194 | + channel = Channel.fromState(client, channelState); |
| 5195 | + }); |
| 5196 | + |
| 5197 | + test( |
| 5198 | + 'should throw StateError when accessing cooldown after disposal', |
| 5199 | + () { |
| 5200 | + // First verify it works when initialized |
| 5201 | + expect(channel.cooldown, equals(0)); |
| 5202 | + |
| 5203 | + // Dispose the channel |
| 5204 | + channel.dispose(); |
| 5205 | + |
| 5206 | + // Now accessing cooldown should throw |
| 5207 | + expect(() => channel.cooldown, throwsA(isA<StateError>())); |
| 5208 | + }, |
| 5209 | + ); |
| 5210 | + |
| 5211 | + test( |
| 5212 | + 'should throw StateError when accessing getRemainingCooldown after disposal', |
| 5213 | + () { |
| 5214 | + // First verify it works when initialized |
| 5215 | + expect(channel.getRemainingCooldown(), equals(0)); |
| 5216 | + |
| 5217 | + // Dispose the channel |
| 5218 | + channel.dispose(); |
| 5219 | + |
| 5220 | + // Now accessing getRemainingCooldown should throw |
| 5221 | + expect(channel.getRemainingCooldown, throwsA(isA<StateError>())); |
| 5222 | + }, |
| 5223 | + ); |
| 5224 | + |
| 5225 | + test( |
| 5226 | + 'should throw StateError when accessing cooldownStream after disposal', |
| 5227 | + () { |
| 5228 | + // First verify it works when initialized |
| 5229 | + expectLater(channel.cooldownStream.take(1), emits(0)); |
| 5230 | + |
| 5231 | + // Dispose the channel |
| 5232 | + channel.dispose(); |
| 5233 | + |
| 5234 | + // Now accessing cooldownStream should throw |
| 5235 | + expect(() => channel.cooldownStream, throwsA(isA<StateError>())); |
| 5236 | + }, |
| 5237 | + ); |
| 5238 | + |
| 5239 | + test( |
| 5240 | + 'should handle race condition scenario - initialization then quick disposal', |
| 5241 | + () { |
| 5242 | + // This test simulates the race condition that was causing the production crash |
| 5243 | + final channelState = _generateChannelState(channelId, channelType); |
| 5244 | + final raceChannel = Channel.fromState(client, channelState); |
| 5245 | + |
| 5246 | + // Verify it works initially |
| 5247 | + expect(raceChannel.cooldown, equals(0)); |
| 5248 | + |
| 5249 | + // Simulate quick disposal (like what happens with rapid navigation) |
| 5250 | + raceChannel.dispose(); |
| 5251 | + |
| 5252 | + // This should throw StateError instead of crashing with null check operator |
| 5253 | + expect(() => raceChannel.cooldown, throwsA(isA<StateError>())); |
| 5254 | + |
| 5255 | + expect(raceChannel.getRemainingCooldown, throwsA(isA<StateError>())); |
| 5256 | + }, |
| 5257 | + ); |
| 5258 | + }); |
| 5259 | + }); |
5098 | 5260 | } |
0 commit comments