Skip to content

Commit ea62340

Browse files
test: Adds subscription tests
1 parent 535e97f commit ea62340

File tree

3 files changed

+1225
-0
lines changed

3 files changed

+1225
-0
lines changed
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
import GraphQL
2+
3+
4+
#if compiler(>=5.5) && canImport(_Concurrency)
5+
6+
@available(macOS 12, iOS 15, watchOS 8, tvOS 15, *)
7+
/// A very simple publish/subscriber used for testing
8+
class SimplePubSub<T> {
9+
private var subscribers: [Subscriber<T>]
10+
11+
init() {
12+
subscribers = []
13+
}
14+
15+
func emit(event: T) {
16+
for subscriber in subscribers {
17+
subscriber.callback(event)
18+
}
19+
}
20+
21+
func cancel() {
22+
for subscriber in subscribers {
23+
subscriber.cancel()
24+
}
25+
}
26+
27+
func subscribe() -> ConcurrentEventStream<T> {
28+
let asyncStream = AsyncThrowingStream<T, Error> { continuation in
29+
let subscriber = Subscriber<T>(
30+
callback: { newValue in
31+
continuation.yield(newValue)
32+
},
33+
cancel: {
34+
continuation.finish()
35+
}
36+
)
37+
subscribers.append(subscriber)
38+
return
39+
}
40+
return ConcurrentEventStream<T>.init(asyncStream)
41+
}
42+
}
43+
44+
struct Subscriber<T> {
45+
let callback: (T) -> Void
46+
let cancel: () -> Void
47+
}
48+
49+
#endif
Lines changed: 206 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,206 @@
1+
import NIO
2+
@testable import GraphQL
3+
4+
#if compiler(>=5.5) && canImport(_Concurrency)
5+
6+
// MARK: Types
7+
struct Email : Encodable {
8+
let from:String
9+
let subject:String
10+
let message:String
11+
let unread:Bool
12+
let priority:Int
13+
14+
init(from:String, subject:String, message:String, unread:Bool, priority:Int = 0) {
15+
self.from = from
16+
self.subject = subject
17+
self.message = message
18+
self.unread = unread
19+
self.priority = priority
20+
}
21+
}
22+
23+
struct Inbox : Encodable {
24+
let emails:[Email]
25+
}
26+
27+
struct EmailEvent : Encodable {
28+
let email:Email
29+
let inbox:Inbox
30+
}
31+
32+
// MARK: Schema
33+
let EmailType = try! GraphQLObjectType(
34+
name: "Email",
35+
fields: [
36+
"from": GraphQLField(
37+
type: GraphQLString
38+
),
39+
"subject": GraphQLField(
40+
type: GraphQLString
41+
),
42+
"message": GraphQLField(
43+
type: GraphQLString
44+
),
45+
"unread": GraphQLField(
46+
type: GraphQLBoolean
47+
),
48+
]
49+
)
50+
let InboxType = try! GraphQLObjectType(
51+
name: "Inbox",
52+
fields: [
53+
"emails": GraphQLField(
54+
type: GraphQLList(EmailType)
55+
),
56+
"total": GraphQLField(
57+
type: GraphQLInt,
58+
resolve: { inbox, _, _, _ in
59+
(inbox as! Inbox).emails.count
60+
}
61+
),
62+
"unread": GraphQLField(
63+
type: GraphQLInt,
64+
resolve: { inbox, _, _, _ in
65+
(inbox as! Inbox).emails.filter({$0.unread}).count
66+
}
67+
),
68+
]
69+
)
70+
let EmailEventType = try! GraphQLObjectType(
71+
name: "EmailEvent",
72+
fields: [
73+
"email": GraphQLField(
74+
type: EmailType
75+
),
76+
"inbox": GraphQLField(
77+
type: InboxType
78+
)
79+
]
80+
)
81+
let EmailQueryType = try! GraphQLObjectType(
82+
name: "Query",
83+
fields: [
84+
"inbox": GraphQLField(
85+
type: InboxType
86+
)
87+
]
88+
)
89+
90+
// MARK: Test Helpers
91+
92+
let eventLoopGroup = MultiThreadedEventLoopGroup(numberOfThreads: 1)
93+
94+
@available(macOS 12, iOS 15, watchOS 8, tvOS 15, *)
95+
class EmailDb {
96+
var emails: [Email]
97+
let publisher: SimplePubSub<Any>
98+
99+
init() {
100+
emails = [
101+
Email(
102+
103+
subject: "Hello",
104+
message: "Hello World",
105+
unread: false
106+
)
107+
]
108+
publisher = SimplePubSub<Any>()
109+
}
110+
111+
/// Adds a new email to the database and triggers all observers
112+
func trigger(email:Email) {
113+
emails.append(email)
114+
publisher.emit(event: email)
115+
}
116+
117+
func stop() {
118+
publisher.cancel()
119+
}
120+
121+
/// Returns the default email schema, with standard resolvers.
122+
func defaultSchema() -> GraphQLSchema {
123+
return emailSchemaWithResolvers(
124+
resolve: {emailAny, _, _, eventLoopGroup, _ throws -> EventLoopFuture<Any?> in
125+
if let email = emailAny as? Email {
126+
return eventLoopGroup.next().makeSucceededFuture(EmailEvent(
127+
email: email,
128+
inbox: Inbox(emails: self.emails)
129+
))
130+
} else {
131+
throw GraphQLError(message: "\(type(of:emailAny)) is not Email")
132+
}
133+
},
134+
subscribe: {_, args, _, eventLoopGroup, _ throws -> EventLoopFuture<Any?> in
135+
let priority = args["priority"].int ?? 0
136+
let filtered = self.publisher.subscribe().stream.filterStream { emailAny throws in
137+
if let email = emailAny as? Email {
138+
return email.priority >= priority
139+
} else {
140+
return true
141+
}
142+
}
143+
return eventLoopGroup.next().makeSucceededFuture(ConcurrentEventStream<Any>(filtered))
144+
}
145+
)
146+
}
147+
148+
/// Generates a subscription to the database using the default schema and resolvers
149+
func subscription (
150+
query:String,
151+
variableValues: [String: Map] = [:]
152+
) throws -> SubscriptionEventStream {
153+
return try createSubscription(schema: defaultSchema(), query: query, variableValues: variableValues)
154+
}
155+
}
156+
157+
/// Generates an email schema with the specified resolve and subscribe methods
158+
func emailSchemaWithResolvers(resolve: GraphQLFieldResolve? = nil, subscribe: GraphQLFieldResolve? = nil) -> GraphQLSchema {
159+
return try! GraphQLSchema(
160+
query: EmailQueryType,
161+
subscription: try! GraphQLObjectType(
162+
name: "Subscription",
163+
fields: [
164+
"importantEmail": GraphQLField(
165+
type: EmailEventType,
166+
args: [
167+
"priority": GraphQLArgument(
168+
type: GraphQLInt
169+
)
170+
],
171+
resolve: resolve,
172+
subscribe: subscribe
173+
)
174+
]
175+
)
176+
)
177+
}
178+
179+
/// Generates a subscription from the given schema and query. It's expected that the resolver/database interactions are configured by the caller.
180+
func createSubscription(
181+
schema: GraphQLSchema,
182+
query: String,
183+
variableValues: [String: Map] = [:]
184+
) throws -> SubscriptionEventStream {
185+
let result = try graphqlSubscribe(
186+
queryStrategy: SerialFieldExecutionStrategy(),
187+
mutationStrategy: SerialFieldExecutionStrategy(),
188+
subscriptionStrategy: SerialFieldExecutionStrategy(),
189+
instrumentation: NoOpInstrumentation,
190+
schema: schema,
191+
request: query,
192+
rootValue: Void(),
193+
context: Void(),
194+
eventLoopGroup: eventLoopGroup,
195+
variableValues: variableValues,
196+
operationName: nil
197+
).wait()
198+
199+
if let stream = result.stream {
200+
return stream
201+
} else {
202+
throw result.errors.first! // We may have more than one...
203+
}
204+
}
205+
206+
#endif

0 commit comments

Comments
 (0)