Skip to content
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
3 changes: 3 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,9 @@ jobs:
steps:
- uses: actions/checkout@v4

- name: Lint
run: swift format lint --recursive . --strict

- name: Build
run: swift build -v

Expand Down
11 changes: 11 additions & 0 deletions .swift-format
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
{
"version": 1,
"indentation": {
"spaces": 4
},
"lineLength": 120,
"maximumBlankLines": 1,
"respectsExistingLineBreaks": true,
"lineBreakBeforeEachArgument": true,
"multiElementCollectionTrailingCommas": true
}
6 changes: 4 additions & 2 deletions Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -17,13 +17,15 @@ let package = Package(
// Products define the executables and libraries a package produces, making them visible to other packages.
.library(
name: "EventSource",
targets: ["EventSource"])
targets: ["EventSource"]
)
],
targets: [
// Targets are the basic building blocks of a package, defining a module or a test suite.
// Targets can depend on other targets in this package and products from dependencies.
.target(
name: "EventSource"),
name: "EventSource"
),
.testTarget(
name: "EventSourceTests",
dependencies: ["EventSource"]
Expand Down
4 changes: 3 additions & 1 deletion Sources/EventSource/EventSource.swift
Original file line number Diff line number Diff line change
Expand Up @@ -396,7 +396,9 @@ public actor EventSource {

// Perform the HTTP request and get an asynchronous byte stream.
let (byteStream, response) = try await session.bytes(
for: currentRequest, delegate: nil)
for: currentRequest,
delegate: nil
)

// Validate HTTP response (status code and content type).
if let httpResponse = response as? HTTPURLResponse {
Expand Down
3 changes: 2 additions & 1 deletion Tests/EventSourceTests/Helpers/MockURLProtocol.swift
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,8 @@ final class MockURLProtocol: URLProtocol, @unchecked Sendable {
/// A test trait to set up and clean up mock URL protocol handlers
struct MockURLSessionTestTrait: TestTrait, TestScoping {
func provideScope(
for test: Test, testCase: Test.Case?,
for test: Test,
testCase: Test.Case?,
performing function: @Sendable () async throws -> Void
) async throws {
// Clear handler before test
Expand Down
25 changes: 17 additions & 8 deletions Tests/EventSourceTests/IntegrationTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -110,7 +110,8 @@ import Testing
#expect((response as? HTTPURLResponse)?.statusCode == 200)
#expect(
(response as? HTTPURLResponse)?.value(forHTTPHeaderField: "Content-Type")
== "text/event-stream")
== "text/event-stream"
)

// Stream events asynchronously
for try await event in byteStream.events {
Expand Down Expand Up @@ -167,7 +168,8 @@ import Testing
#expect(request.value(forHTTPHeaderField: "Content-Type") == "application/json")
#expect(
request.value(forHTTPHeaderField: "Authorization")?.starts(with: "Bearer ")
== true)
== true
)

// Verify request body
if let bodyData = request.httpBody {
Expand Down Expand Up @@ -209,7 +211,8 @@ import Testing
#expect((response as? HTTPURLResponse)?.statusCode == 200)
#expect(
(response as? HTTPURLResponse)?.value(forHTTPHeaderField: "Content-Type")
== "text/event-stream")
== "text/event-stream"
)

// Stream events asynchronously
for try await event in byteStream.events {
Expand Down Expand Up @@ -540,7 +543,8 @@ import Testing
#expect((response as? HTTPURLResponse)?.statusCode == 200)
#expect(
(response as? HTTPURLResponse)?.value(forHTTPHeaderField: "Content-Type")
== "text/event-stream")
== "text/event-stream"
)

// Use the events extension to convert bytes to SSE events
let eventsSequence = byteStream.events
Expand Down Expand Up @@ -717,7 +721,9 @@ import Testing

// Create EventSource with the custom session
let eventSource = EventSource(
request: request, configuration: session.configuration)
request: request,
configuration: session.configuration
)

// Set up error handler
let errorTracker = ErrorTracker()
Expand Down Expand Up @@ -794,7 +800,8 @@ import Testing
#expect((response as? HTTPURLResponse)?.statusCode == 200)
#expect(
(response as? HTTPURLResponse)?.value(forHTTPHeaderField: "Content-Type")
== "text/event-stream")
== "text/event-stream"
)

// Stream events asynchronously
for try await event in byteStream.events {
Expand Down Expand Up @@ -857,7 +864,8 @@ import Testing
// Reference to the protocol instance (need to do this via reflection)
guard
let protocolInstance = request.value(
forHTTPHeaderField: "_MockURLProtocolInstance") as? NSObjectProtocol
forHTTPHeaderField: "_MockURLProtocolInstance"
) as? NSObjectProtocol
else {
return
}
Expand Down Expand Up @@ -923,7 +931,8 @@ import Testing
}

func setClientAndProtocol(
client: NSObjectProtocol, protocolInstance: NSObjectProtocol
client: NSObjectProtocol,
protocolInstance: NSObjectProtocol
) {
self.client = client
self.protocolInstance = protocolInstance
Expand Down
40 changes: 27 additions & 13 deletions Tests/EventSourceTests/ParserTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,8 @@ struct ParserTests {
// Check that lastEventId was not updated with the NUL-containing ID
#expect(
await parser.getLastEventId() == "",
"LastEventId should not be set to a value containing NUL.")
"LastEventId should not be set to a value containing NUL."
)

// Check the dispatched event
var dispatchedEvents: [EventSource.Event] = []
Expand All @@ -94,7 +95,8 @@ struct ParserTests {
#expect(dispatchedEvents.count == 1)
#expect(
dispatchedEvents.first?.id == nil,
"Event's ID field should be nil because the raw ID field contained NUL.")
"Event's ID field should be nil because the raw ID field contained NUL."
)
#expect(dispatchedEvents.first?.data == "test")
}

Expand All @@ -120,11 +122,13 @@ struct ParserTests {
// After "id: " line, lastEventId should be reset to empty string
#expect(
await parser.getLastEventId() == "",
"lastEventID should be reset to empty string by an 'id:' line.")
"lastEventID should be reset to empty string by an 'id:' line."
)

#expect(
events[1].id == "", // Changed from nil to empty string
"Second event's currentEventId should be empty string as 'id:' resets it.")
"Second event's currentEventId should be empty string as 'id:' resets it."
)
#expect(events[1].data == "event2")
}

Expand Down Expand Up @@ -157,7 +161,8 @@ struct ParserTests {
#expect(events.first?.data == "test data")
#expect(
events.first?.retry == 5000,
"The retry value should be part of the event if dispatched with it.")
"The retry value should be part of the event if dispatched with it."
)
}

@Test("Retry field only updates reconnection time")
Expand All @@ -170,7 +175,8 @@ struct ParserTests {

#expect(
events.isEmpty,
"A message containing only a 'retry' field should not produce an event.")
"A message containing only a 'retry' field should not produce an event."
)
#expect(await parser.getReconnectionTime() == 1234)
#expect(initialReconnectionTime != 1234)
}
Expand Down Expand Up @@ -294,7 +300,8 @@ struct ParserTests {
#expect(events.count == 1)
#expect(
events.first?.data == "test\u{FFFD}string",
"Invalid UTF-8 byte should be replaced by replacement character.")
"Invalid UTF-8 byte should be replaced by replacement character."
)
}

@Test("Field without colon is ignored")
Expand Down Expand Up @@ -387,21 +394,25 @@ struct ParserTests {
#expect(events.first?.event == "", "Event type from 'event' line without value.")
#expect(
events.first?.id == "", // Changed from nil to empty string
"currentEventId should be empty string as 'id' line had no value.")
"currentEventId should be empty string as 'id' line had no value."
)
#expect(
await parser.getLastEventId() == "",
"LastEventId should be empty string from 'id' line without value.")
"LastEventId should be empty string from 'id' line without value."
)
#expect(
await parser.getReconnectionTime() == initialReconnectTime,
"Empty 'retry' field should not change reconnection time.")
"Empty 'retry' field should not change reconnection time."
)
}
}

@Suite("Comment and Line Tests")
struct CommentAndLineTests {
@Test("Only comment lines and empty lines produce no events")
func testOnlyCommentLinesAndEmptyLinesProduceNoEvents() async {
let stream = ":comment\n\n:another comment\r\n\r\n" // Includes dispatch-triggering empty lines but no data fields
// Includes dispatch-triggering empty lines but no data fields
let stream = ":comment\n\n:another comment\r\n\r\n"
let events = await getEvents(from: stream)
#expect(
events.isEmpty,
Expand Down Expand Up @@ -463,7 +474,9 @@ struct ParserTests {
let stream = "data: final event" // No trailing newline or blank line
let events = await getEvents(from: stream)
#expect(
events.count == 1, "Event should be dispatched based on current finish() logic.")
events.count == 1,
"Event should be dispatched based on current finish() logic."
)
#expect(events.first?.data == "final event")

let streamWithID = "id: lastid\ndata: final event with id"
Expand Down Expand Up @@ -526,7 +539,8 @@ struct ParserTests {
fullStreamBytes.append(
contentsOf: createSSEMessage(fields: [
(name: "data", value: "test with bom")
]))
])
)

let parser = EventSource.Parser()
// Process each byte individually
Expand Down