Skip to content

Commit a711b20

Browse files
authored
[126109] Add mock end to end network tests (#34)
1 parent 66f0466 commit a711b20

File tree

4 files changed

+341
-36
lines changed

4 files changed

+341
-36
lines changed

LDSwiftEventSource.xcodeproj/project.pbxproj

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
B49B5E5B24668031008BF867 /* LDSwiftEventSourceTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = B49B5E4324667F43008BF867 /* LDSwiftEventSourceTests.swift */; };
1717
B49B5E67246684B9008BF867 /* LDSwiftEventSource.h in Headers */ = {isa = PBXBuildFile; fileRef = B49B5E65246684B9008BF867 /* LDSwiftEventSource.h */; settings = {ATTRIBUTES = (Public, ); }; };
1818
B49B5E72246C4796008BF867 /* LDSwiftEventSource.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = B49B5DFC24667D41008BF867 /* LDSwiftEventSource.framework */; };
19+
B4BCAE6E272753FA000EBD43 /* TestUtil.swift in Sources */ = {isa = PBXBuildFile; fileRef = B4BCAE6D272753FA000EBD43 /* TestUtil.swift */; };
1920
B4C29CC826FF743D008B6DE2 /* Logs.swift in Sources */ = {isa = PBXBuildFile; fileRef = B4C29CC726FF743C008B6DE2 /* Logs.swift */; };
2021
/* End PBXBuildFile section */
2122

@@ -47,6 +48,7 @@
4748
B49B5E6E2466875F008BF867 /* LICENSE.txt */ = {isa = PBXFileReference; lastKnownFileType = text; path = LICENSE.txt; sourceTree = "<group>"; };
4849
B49B5E6F2466875F008BF867 /* README.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = README.md; sourceTree = "<group>"; };
4950
B49B5E702466875F008BF867 /* Package.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Package.swift; sourceTree = "<group>"; };
51+
B4BCAE6D272753FA000EBD43 /* TestUtil.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TestUtil.swift; sourceTree = "<group>"; };
5052
B4C29CC726FF743C008B6DE2 /* Logs.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Logs.swift; sourceTree = "<group>"; };
5153
/* End PBXFileReference section */
5254

@@ -94,6 +96,7 @@
9496
B49B5E4024667F43008BF867 /* EventParserTests.swift */,
9597
B49B5E4224667F43008BF867 /* UTF8LineParserTests.swift */,
9698
B49B5E4324667F43008BF867 /* LDSwiftEventSourceTests.swift */,
99+
B4BCAE6D272753FA000EBD43 /* TestUtil.swift */,
97100
);
98101
path = Tests;
99102
sourceTree = "<group>";
@@ -272,6 +275,7 @@
272275
B49B5E5824668031008BF867 /* EventParserTests.swift in Sources */,
273276
B49B5E5A24668031008BF867 /* UTF8LineParserTests.swift in Sources */,
274277
B49B5E5B24668031008BF867 /* LDSwiftEventSourceTests.swift in Sources */,
278+
B4BCAE6E272753FA000EBD43 /* TestUtil.swift in Sources */,
275279
);
276280
runOnlyForDeploymentPostprocessing = 0;
277281
};

Source/LDSwiftEventSource.swift

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -246,7 +246,9 @@ class EventSourceDelegate: NSObject, URLSessionDataDelegate {
246246
eventParser.reset()
247247

248248
if let error = error {
249-
if readyState != .shutdown && errorHandlerAction != .shutdown {
249+
// Ignore cancelled error
250+
if (error as NSError).code == NSURLErrorCancelled {
251+
} else if readyState != .shutdown && errorHandlerAction != .shutdown {
250252
logger.log(.info, "Connection error: %@", error.localizedDescription)
251253
errorHandlerAction = dispatchError(error: error)
252254
} else {

Tests/LDSwiftEventSourceTests.swift

Lines changed: 215 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,33 @@
11
import XCTest
22
@testable import LDSwiftEventSource
33

4+
#if os(Linux)
5+
import FoundationNetworking
6+
#endif
7+
48
final class LDSwiftEventSourceTests: XCTestCase {
9+
private var mockHandler: MockHandler!
10+
11+
override func setUp() {
12+
super.setUp()
13+
mockHandler = MockHandler()
14+
XCTAssertTrue(URLProtocol.registerClass(MockingProtocol.self))
15+
}
16+
17+
override func tearDown() {
18+
super.tearDown()
19+
URLProtocol.unregisterClass(MockingProtocol.self)
20+
// Enforce that tests consume all mocked network requests
21+
MockingProtocol.requested.expectNoEvent(within: 0.01)
22+
MockingProtocol.resetRequested()
23+
// Enforce that tests consume all calls to the mock handler
24+
mockHandler.events.expectNoEvent(within: 0.01)
25+
mockHandler = nil
26+
}
27+
528
func testConfigDefaults() {
629
let url = URL(string: "abc")!
7-
let config = EventSource.Config(handler: MockHandler(), url: url)
30+
let config = EventSource.Config(handler: mockHandler, url: url)
831
XCTAssertEqual(config.url, url)
932
XCTAssertEqual(config.method, "GET")
1033
XCTAssertEqual(config.body, nil)
@@ -20,7 +43,7 @@ final class LDSwiftEventSourceTests: XCTestCase {
2043

2144
func testConfigModification() {
2245
let url = URL(string: "abc")!
23-
var config = EventSource.Config(handler: MockHandler(), url: url)
46+
var config = EventSource.Config(handler: mockHandler, url: url)
2447

2548
let testBody = "test data".data(using: .utf8)
2649
let testHeaders = ["Authorization": "basic abc"]
@@ -50,7 +73,7 @@ final class LDSwiftEventSourceTests: XCTestCase {
5073
}
5174

5275
func testConfigUrlSession() {
53-
var config = EventSource.Config(handler: MockHandler(), url: URL(string: "abc")!)
76+
var config = EventSource.Config(handler: mockHandler, url: URL(string: "abc")!)
5477
let defaultSessionConfig = config.urlSessionConfiguration
5578
XCTAssertEqual(defaultSessionConfig.timeoutIntervalForRequest, 300.0)
5679
XCTAssertEqual(defaultSessionConfig.httpAdditionalHeaders?["Accept"] as? String, "text/event-stream")
@@ -71,7 +94,7 @@ final class LDSwiftEventSourceTests: XCTestCase {
7194
}
7295

7396
func testLastEventIdFromConfig() {
74-
var config = EventSource.Config(handler: MockHandler(), url: URL(string: "abc")!)
97+
var config = EventSource.Config(handler: mockHandler, url: URL(string: "abc")!)
7598
var es = EventSource(config: config)
7699
XCTAssertEqual(es.getLastEventId(), nil)
77100
config.lastEventId = "def"
@@ -80,7 +103,7 @@ final class LDSwiftEventSourceTests: XCTestCase {
80103
}
81104

82105
func testCreatedSession() {
83-
let config = EventSource.Config(handler: MockHandler(), url: URL(string: "abc")!)
106+
let config = EventSource.Config(handler: mockHandler, url: URL(string: "abc")!)
84107
let session = EventSourceDelegate(config: config).createSession()
85108
XCTAssertEqual(session.configuration.timeoutIntervalForRequest, config.idleTimeout)
86109
XCTAssertEqual(session.configuration.httpAdditionalHeaders?["Accept"] as? String, "text/event-stream")
@@ -89,7 +112,7 @@ final class LDSwiftEventSourceTests: XCTestCase {
89112

90113
func testCreateRequest() {
91114
// 192.0.2.1 is assigned as TEST-NET-1 reserved usage.
92-
var config = EventSource.Config(handler: MockHandler(), url: URL(string: "http://192.0.2.1")!)
115+
var config = EventSource.Config(handler: mockHandler, url: URL(string: "http://192.0.2.1")!)
93116
// Testing default configs
94117
var request = EventSourceDelegate(config: config).createRequest()
95118
XCTAssertEqual(request.url, config.url)
@@ -119,28 +142,203 @@ final class LDSwiftEventSourceTests: XCTestCase {
119142
}
120143

121144
func testDispatchError() {
122-
let handler = MockHandler()
123145
var connectionErrorHandlerCallCount = 0
124146
var connectionErrorAction: ConnectionErrorAction = .proceed
125-
var config = EventSource.Config(handler: handler, url: URL(string: "abc")!)
147+
var config = EventSource.Config(handler: mockHandler, url: URL(string: "abc")!)
126148
config.connectionErrorHandler = { _ in
127149
connectionErrorHandlerCallCount += 1
128150
return connectionErrorAction
129151
}
130152
let es = EventSourceDelegate(config: config)
131153
XCTAssertEqual(es.dispatchError(error: DummyError()), .proceed)
132154
XCTAssertEqual(connectionErrorHandlerCallCount, 1)
133-
guard case .error(let err) = handler.takeEvent(), err is DummyError
155+
guard case .error(let err) = mockHandler.events.expectEvent(), err is DummyError
134156
else {
135157
XCTFail("handler should receive error if EventSource is not shutting down")
136158
return
137159
}
138-
XCTAssertTrue(handler.receivedEvents.isEmpty)
160+
mockHandler.events.expectNoEvent()
139161
connectionErrorAction = .shutdown
140162
XCTAssertEqual(es.dispatchError(error: DummyError()), .shutdown)
141163
XCTAssertEqual(connectionErrorHandlerCallCount, 2)
142-
XCTAssertTrue(handler.receivedEvents.isEmpty)
143164
}
165+
166+
func sessionWithMockProtocol() -> URLSessionConfiguration {
167+
let sessionConfig = URLSessionConfiguration.default
168+
sessionConfig.protocolClasses = [MockingProtocol.self] + (sessionConfig.protocolClasses ?? [])
169+
return sessionConfig
170+
}
171+
172+
#if !os(Linux)
173+
func testStartDefaultRequest() {
174+
var config = EventSource.Config(handler: mockHandler, url: URL(string: "http://example.com")!)
175+
config.urlSessionConfiguration = sessionWithMockProtocol()
176+
let es = EventSource(config: config)
177+
es.start()
178+
let handler = MockingProtocol.requested.expectEvent()
179+
XCTAssertEqual(handler.request.url, config.url)
180+
XCTAssertEqual(handler.request.httpMethod, config.method)
181+
XCTAssertEqual(handler.request.httpBody, config.body)
182+
XCTAssertEqual(handler.request.timeoutInterval, config.idleTimeout)
183+
XCTAssertEqual(handler.request.allHTTPHeaderFields?["Accept"], "text/event-stream")
184+
XCTAssertEqual(handler.request.allHTTPHeaderFields?["Cache-Control"], "no-cache")
185+
XCTAssertNil(handler.request.allHTTPHeaderFields?["Last-Event-Id"])
186+
es.stop()
187+
}
188+
189+
func testStartRequestWithConfiguration() {
190+
var config = EventSource.Config(handler: mockHandler, url: URL(string: "http://example.com")!)
191+
config.urlSessionConfiguration = sessionWithMockProtocol()
192+
config.method = "REPORT"
193+
config.body = Data("test body".utf8)
194+
config.idleTimeout = 500.0
195+
config.lastEventId = "abc"
196+
config.headers = ["X-LD-Header": "def"]
197+
let es = EventSource(config: config)
198+
es.start()
199+
let handler = MockingProtocol.requested.expectEvent()
200+
XCTAssertEqual(handler.request.url, config.url)
201+
XCTAssertEqual(handler.request.httpMethod, config.method)
202+
XCTAssertEqual(handler.request.bodyStreamAsData(), config.body)
203+
XCTAssertEqual(handler.request.timeoutInterval, config.idleTimeout)
204+
XCTAssertEqual(handler.request.allHTTPHeaderFields?["Accept"], "text/event-stream")
205+
XCTAssertEqual(handler.request.allHTTPHeaderFields?["Cache-Control"], "no-cache")
206+
XCTAssertEqual(handler.request.allHTTPHeaderFields?["Last-Event-Id"], config.lastEventId)
207+
XCTAssertEqual(handler.request.allHTTPHeaderFields?["X-LD-Header"], "def")
208+
es.stop()
209+
}
210+
211+
func testSuccessfulResponseOpens() {
212+
var config = EventSource.Config(handler: mockHandler, url: URL(string: "http://example.com")!)
213+
config.urlSessionConfiguration = sessionWithMockProtocol()
214+
let es = EventSource(config: config)
215+
es.start()
216+
let handler = MockingProtocol.requested.expectEvent()
217+
handler.respond(statusCode: 200)
218+
XCTAssertEqual(mockHandler.events.expectEvent(), .opened)
219+
es.stop()
220+
XCTAssertEqual(mockHandler.events.expectEvent(), .closed)
221+
}
222+
223+
func testLastEventIdUpdatedByEvents() {
224+
var config = EventSource.Config(handler: mockHandler, url: URL(string: "http://example.com")!)
225+
config.urlSessionConfiguration = sessionWithMockProtocol()
226+
config.reconnectTime = 0.1
227+
let es = EventSource(config: config)
228+
es.start()
229+
let handler = MockingProtocol.requested.expectEvent()
230+
handler.respond(statusCode: 200)
231+
XCTAssertEqual(mockHandler.events.expectEvent(), .opened)
232+
XCTAssertNil(es.getLastEventId())
233+
handler.respond(didLoad: "id: abc\n\n")
234+
// Comment used for synchronization
235+
handler.respond(didLoad: ":comment\n")
236+
XCTAssertEqual(mockHandler.events.expectEvent(), .comment("comment"))
237+
XCTAssertEqual(es.getLastEventId(), "abc")
238+
handler.finish()
239+
XCTAssertEqual(mockHandler.events.expectEvent(), .closed)
240+
// Expect to reconnect and include new event id
241+
let reconnectHandler = MockingProtocol.requested.expectEvent()
242+
XCTAssertEqual(reconnectHandler.request.allHTTPHeaderFields?["Last-Event-Id"], "abc")
243+
es.stop()
244+
}
245+
246+
func testUsesRetryTime() {
247+
var config = EventSource.Config(handler: mockHandler, url: URL(string: "http://example.com")!)
248+
config.urlSessionConfiguration = sessionWithMockProtocol()
249+
// Long enough to cause a timeout if the retry time is not updated
250+
config.reconnectTime = 5
251+
let es = EventSource(config: config)
252+
es.start()
253+
let handler = MockingProtocol.requested.expectEvent()
254+
handler.respond(statusCode: 200)
255+
XCTAssertEqual(mockHandler.events.expectEvent(), .opened)
256+
handler.respond(didLoad: "retry: 100\n\n")
257+
handler.finish()
258+
XCTAssertEqual(mockHandler.events.expectEvent(), .closed)
259+
// Expect to reconnect before this times out
260+
_ = MockingProtocol.requested.expectEvent()
261+
es.stop()
262+
}
263+
264+
func testCallsHandlerWithMessage() {
265+
var config = EventSource.Config(handler: mockHandler, url: URL(string: "http://example.com")!)
266+
config.urlSessionConfiguration = sessionWithMockProtocol()
267+
let es = EventSource(config: config)
268+
es.start()
269+
let handler = MockingProtocol.requested.expectEvent()
270+
handler.respond(statusCode: 200)
271+
XCTAssertEqual(mockHandler.events.expectEvent(), .opened)
272+
handler.respond(didLoad: "event: custom\ndata: {}\n\n")
273+
XCTAssertEqual(mockHandler.events.expectEvent(), .message("custom", MessageEvent(data: "{}", lastEventId: nil)))
274+
es.stop()
275+
XCTAssertEqual(mockHandler.events.expectEvent(), .closed)
276+
}
277+
278+
func testRetryOnInvalidResponseCode() {
279+
var config = EventSource.Config(handler: mockHandler, url: URL(string: "http://example.com")!)
280+
config.urlSessionConfiguration = sessionWithMockProtocol()
281+
config.reconnectTime = 0.1
282+
let es = EventSource(config: config)
283+
es.start()
284+
let handler = MockingProtocol.requested.expectEvent()
285+
handler.respond(statusCode: 400)
286+
guard case let .error(err) = mockHandler.events.expectEvent(),
287+
let responseErr = err as? UnsuccessfulResponseError
288+
else {
289+
XCTFail("Expected UnsuccessfulResponseError to be given to handler")
290+
return
291+
}
292+
XCTAssertEqual(responseErr.responseCode, 400)
293+
// Expect the client to reconnect
294+
_ = MockingProtocol.requested.expectEvent()
295+
es.stop()
296+
}
297+
298+
func testShutdownByErrorHandlerOnInitialErrorResponse() {
299+
var config = EventSource.Config(handler: mockHandler, url: URL(string: "http://example.com")!)
300+
config.urlSessionConfiguration = sessionWithMockProtocol()
301+
config.reconnectTime = 0.1
302+
config.connectionErrorHandler = { err in
303+
if let responseErr = err as? UnsuccessfulResponseError {
304+
XCTAssertEqual(responseErr.responseCode, 400)
305+
} else {
306+
XCTFail("Expected UnsuccessfulResponseError to be given to handler")
307+
}
308+
return .shutdown
309+
}
310+
let es = EventSource(config: config)
311+
es.start()
312+
let handler = MockingProtocol.requested.expectEvent()
313+
handler.respond(statusCode: 400)
314+
// Expect the client not to reconnect
315+
MockingProtocol.requested.expectNoEvent(within: 1.0)
316+
es.stop()
317+
// Error should not have been given to the handler
318+
mockHandler.events.expectNoEvent()
319+
}
320+
321+
func testShutdownByErrorHandlerOnResponseCompletionError() {
322+
var config = EventSource.Config(handler: mockHandler, url: URL(string: "http://example.com")!)
323+
config.urlSessionConfiguration = sessionWithMockProtocol()
324+
config.reconnectTime = 0.1
325+
config.connectionErrorHandler = { _ in
326+
return .shutdown
327+
}
328+
let es = EventSource(config: config)
329+
es.start()
330+
let handler = MockingProtocol.requested.expectEvent()
331+
handler.respond(statusCode: 200)
332+
XCTAssertEqual(mockHandler.events.expectEvent(), .opened)
333+
handler.finishWith(error: DummyError())
334+
XCTAssertEqual(mockHandler.events.expectEvent(), .closed)
335+
// Expect the client not to reconnect
336+
MockingProtocol.requested.expectNoEvent(within: 1.0)
337+
es.stop()
338+
// Error should not have been given to the handler
339+
mockHandler.events.expectNoEvent()
340+
}
341+
#endif
144342
}
145343

146344
private enum ReceivedEvent: Equatable {
@@ -165,31 +363,13 @@ private enum ReceivedEvent: Equatable {
165363
}
166364

167365
private class MockHandler: EventHandler {
168-
var receivedEvents: [ReceivedEvent] = []
366+
var events = EventSink<ReceivedEvent>()
169367

170-
func onOpened() {
171-
receivedEvents.append(.opened)
172-
}
173-
174-
func onClosed() {
175-
receivedEvents.append(.closed)
176-
}
177-
178-
func onMessage(eventType: String, messageEvent: MessageEvent) {
179-
receivedEvents.append(.message(eventType, messageEvent))
180-
}
181-
182-
func onComment(comment: String) {
183-
receivedEvents.append(.comment(comment))
184-
}
185-
186-
func onError(error: Error) {
187-
receivedEvents.append(.error(error))
188-
}
189-
190-
func takeEvent() -> ReceivedEvent {
191-
receivedEvents.remove(at: 0)
192-
}
368+
func onOpened() { events.record(.opened) }
369+
func onClosed() { events.record(.closed) }
370+
func onMessage(eventType: String, messageEvent: MessageEvent) { events.record(.message(eventType, messageEvent)) }
371+
func onComment(comment: String) { events.record(.comment(comment)) }
372+
func onError(error: Error) { events.record(.error(error)) }
193373
}
194374

195375
private class DummyError: Error { }

0 commit comments

Comments
 (0)