Skip to content
Draft
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
1 change: 1 addition & 0 deletions Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ let package = Package(
.product(name: "JWT", package: "jwt"),
]),
.testTarget(name: "FCMTests", dependencies: [
.product(name: "VaporTesting", package: "vapor"),
.target(name: "FCM"),
]),
]
Expand Down
95 changes: 95 additions & 0 deletions Tests/FCMTests/FCMConcurrencyTests.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
import JWTKit
import Testing
import VaporTesting
@testable import FCM

@Suite("FCM Concurrency Tests")
struct FCMConcurrencyTests {
@Test(
"Sending message right away should not crash",
.bug("https://github.com/MihaelIsaev/FCM/issues/57")
)
func sendMessageRightAwayAndDoNotCrash() async throws {
let message = FCMMessageDefault(token: "SOME_TOKEN", notification: nil)
try await withApp { app in
// Preload the request so we may use it right away
let request = request(on: app)
app.clients.use(.fcmTestResponder(.default))
app.fcm.configuration = .testing
_ = try await request.fcm.send(message)
try await waitForCacheWarmup()
}
}

@Test(
"Shutting the application down right away should not crash",
.bug("https://github.com/MihaelIsaev/FCM/issues/57")
)
func shutApplicationDownRightAwayAndDoNotCrash() async throws {
for _ in 0..<1000 {
try await withApp { app in
app.fcm.configuration = .testing
}
}
try await waitForCacheWarmup()
}

@Test(
"Resetting the configuration should not crash",
.bug("https://github.com/MihaelIsaev/FCM/issues/57")
)
func resetConfigurationAndDoNotCrash() async throws {
try await withApp { app in
for _ in 0..<1000 {
app.fcm.configuration = .testing
app.fcm.configuration = nil
}
try await waitForCacheWarmup()
}
}

@Test(
"Changing the configuration should invalidate credentials",
.bug("https://github.com/MihaelIsaev/FCM/issues/57")
)
func changeConfigurationAndInvalidateCredentials() async throws {
let newAccessToken = "NEW_ACCESS_TOKEN"
let newMessageName = "NEW_MESSAGE_NAME"
let newEmail = "new@test.aim.gserviceaccount.com"

var newResponder = FCMTestResponder.default
newResponder.accessToken { request in
let jwt = try #require(request.content.decode([String: String].self)["assertion"])
let payload = try await JWTKeyCollection().unverified(jwt, as: GAuthPayload.self)
#expect(payload.iss.value == newEmail)
return newAccessToken
}
newResponder.sendMessage { request in
#expect(request.headers.bearerAuthorization?.token == newAccessToken)
return newMessageName
}

let message = FCMMessageDefault(token: "SOME_TOKEN", notification: nil)
try await withApp { app in
app.clients.use(.fcmTestResponder(.default))
app.fcm.configuration = .testing
try await waitForCacheWarmup()
_ = try await request(on: app).fcm.send(message)

app.clients.use(.fcmTestResponder(newResponder))
app.fcm.configuration = .testing(email: newEmail)
try await waitForCacheWarmup()
_ = try await request(on: app).fcm.send(message)
}
}

func request(on app: Application) -> Request {
Request(application: app, on: app.eventLoopGroup.any())
}

func waitForCacheWarmup() async throws {
// FCM perform a background task after changing the configuration.
// Not waiting for task to complete may cause race conditions.
try await Task.sleep(for: .milliseconds(100))
}
}
42 changes: 42 additions & 0 deletions Tests/FCMTests/FCMConfiguration+Testing.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import FCM

extension FCMConfiguration {
static let testing = FCMConfiguration.testing(email: "testing@test.aim.gserviceaccount.com")

static func testing(email: String) -> FCMConfiguration {
FCMConfiguration(
email: email,
projectId: "test",
key: """
-----BEGIN PRIVATE KEY-----
MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQCq2nhnp0uox5Yj
qEfv7BWYFD1VsyI+NksQSEp0lv2Jmq2DjtE3x7YklWCJimUXSpsRkuuTSfZigLvq
nWpC4v0YpYJcCwF2/MET2K9Qs0+qetETTb3JAuyPH7obC+gEtKQ5D5ZqSBHGy2xH
2kdCNIxH4fKqLcSR4IbzsXU10dARGyTugKpeVZ67FmcSxZtvEQIXwrCoKRVi8h78
aiaEGl6PWjPEp2tTo1iSHixfcEXAaqFtYjiZAcoYcVITR7dCmIj66Dxu2tHYWWgY
wMGL7d8VIADgtxJgt7lb1Bu6YuFxehM21N4jJFCSfJSwtzJXSAPCA/OH+VCCMirj
q760//u/AgMBAAECggEAAQeeECne9950FjTuchC/NJJyqDCTNULIgwmcgUVjs8+d
2hwjQK3QeDn6Qfn2kARgGOQEzXd1p7RU7Z4TROHvWpWsync6hAgT9dWpgNgD0+g3
mGEwkqSU3mv3iDAzLswT7VAdvPhAOy2AspIrOcftTIWdG894ztRGm/Nm3HMuSNwZ
e8PChu3fCa0CzQSmlHDBOjY0a+chl9T+3tBPaKjeTtgr2dhdSO45qlqaZFewWXJF
e6lgn49MpfsAQt22ohSurts40uOv+BDdX6eSEeN19PLgFbQ41adKfrpz2qBWAIga
tGYTwLuvcIXWnvaweI2G1vAkdUyVWl/xjNq+U64pSQKBgQDsDVWHiRHW2/UE/CL/
gTsEJvjZIFERYNrtwSNhYH+M+XVjrKWxo+6MLZ+A4O4MWfgz4x0wUFW+L7r2I7iF
KG/377OC6xWrhOL23oWdB+yWfr55eP6Y/HD15F+Z3dUR6H4i6kgdfykgyI6TepX3
phtdjznPWMbnXcaGDibEmd6xvQKBgQC5Sqa7/Ipx97oGr/FKNY7qhOxt0gRpyK0Y
ui/xOsG5yro8UOZruRjeKf3cyPzqPoKtGPE+HfKN/vma40MLWXfJukE+849y9j5r
tMIgVFmGii0C0SmK+Jw3dtdKEt8QblfQ3TfAw3RFCo9J7VdjXZIlkrhiw5XLrkYa
RXNcV7w1KwKBgH+Z+akppHYUMxA9yCF8V024T3735DrTs6UgaaLDClBHrXhzJKKx
bktigj2l2ajdnblWxTmPw7nqjVNvHdkFcfmCHvTfZbhxPkubIHkxhmgYHZkGmgJT
PDEAAdnoO7zRhBYVtWQUkEQDhmcctiLILTTXLrXyVJtPavief8B5ORO1AoGAUPQd
nro6XoqmGu/Z0ttNgobqqRx90x3bCpemBJXwN9Urwthxo5TuGXptMH4bidgfzbK9
C6+X3pQMx7ANBbNkE52tjexpuwd8xB/oRKm1p4NNIRLzPIVb8xuX+gP+szYSZe2Q
w0Zh0RxI+Dqa2I30IThWGMhs9N1CQY4gVbL7RpsCgYEAlUvnGI3YOMtb9COd5SrM
B/X+A9wifXF9+cTgngCdprJYv+VekWUSWa+jrMTbC2jURTtuOZzqgqNVdKNmfeUG
YntMAgkUKphW58LC5uU8VziNp686h40AQXQHAKXz984MWB1PpQgUria3ArYXlI0D
rG5mv3FiiqQSimO0AMir0JQ=
-----END PRIVATE KEY-----
"""
)
}
}
78 changes: 78 additions & 0 deletions Tests/FCMTests/FCMTestResponder.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
import Vapor

struct FCMTestResponder: Sendable {
mutating func respond(to url: URI, with response: ClientResponse) {
respond(to: url) { _ in response }
}

mutating func respond(to url: URI, with response: @Sendable @escaping (ClientRequest) async throws -> ClientResponse) {
responses[url] = response
}

mutating func accessToken(with perform: @Sendable @escaping (ClientRequest) async throws -> String) {
respond(to: "https://www.googleapis.com/oauth2/v4/token") { request in
let accessToken = try await perform(request)
var response = ClientResponse()
try response.content.encode(["access_token": accessToken])
return response
}
}

mutating func sendMessage(with perform: @Sendable @escaping (ClientRequest) async throws -> String) {
respond(to: "https://fcm.googleapis.com/v1/projects/test/messages:send") { request in
let name = try await perform(request)
var response = ClientResponse()
try response.content.encode(["name": name])
return response
}
}

func client(application: Application) -> Client {
Client(application: application, responder: self)
}

static let `default`: FCMTestResponder = {
var responder = FCMTestResponder()
responder.accessToken { _ in "DEFAULT_ACCESS_TOKEN" }
responder.sendMessage { _ in "DEFAULT_MESSAGE_NAME" }
return responder
}()

fileprivate var responses: [URI: @Sendable (ClientRequest) async throws -> ClientResponse] = [:]
}

extension FCMTestResponder {
struct Client: Vapor.Client {
let application: Application
let responder: FCMTestResponder

var eventLoop: EventLoop {
application.eventLoopGroup.any()
}

func delegating(to eventLoop: EventLoop) -> Vapor.Client {
self
}

func send(_ request: ClientRequest) -> EventLoopFuture<ClientResponse> {
let url = request.url
guard let response = responder.responses[url] else {
let error = Abort(.notImplemented, reason: "No registered response for url: '\(url)'.")
return eventLoop.future(error: error)
}
return eventLoop.makeFutureWithTask {
try await response(request)
}
}
}
}

extension Application.Clients.Provider {
static func fcmTestResponder(_ responder: FCMTestResponder) -> Application.Clients.Provider {
.init { application in
application.clients.use { application in
responder.client(application: application)
}
}
}
}
10 changes: 0 additions & 10 deletions Tests/FCMTests/FCMTests.swift

This file was deleted.