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
14 changes: 14 additions & 0 deletions Jared.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,9 @@
B319236224DE76A2001904BA /* JaredUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = B319236124DE76A2001904BA /* JaredUITests.swift */; };
B32DE51524D51F9C00AB3A71 /* RouterTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = B32DE51424D51F9C00AB3A71 /* RouterTests.swift */; };
EC71CM00000000000000A002 /* CoreModuleTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = EC71CM00000000000000A001 /* CoreModuleTests.swift */; };
RT00TEST0000000000002BF0 /* RouteTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = RT00TEST0000000000001FRF /* RouteTests.swift */; };
WRF0TEST0000000000002BF0 /* WebhookRouteFactoryTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = WRF0TEST0000000000001FRF /* WebhookRouteFactoryTests.swift */; };
PM00TEST0000000000002BF0 /* PluginManagerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = PM00TEST0000000000001FRF /* PluginManagerTests.swift */; };
B32DE51624D522DA00AB3A71 /* Router.swift in Sources */ = {isa = PBXBuildFile; fileRef = B3BF17C1244EBE9A00CC44C5 /* Router.swift */; };
EC70RF00000000000000A004 /* RuntimeFlags.swift in Sources */ = {isa = PBXBuildFile; fileRef = EC70RF00000000000000A001 /* RuntimeFlags.swift */; };
EC70FP00000000000000A004 /* InboundFilterPolicy.swift in Sources */ = {isa = PBXBuildFile; fileRef = EC70FP00000000000000A001 /* InboundFilterPolicy.swift */; };
Expand Down Expand Up @@ -80,6 +83,7 @@
B38BB6AD24EB99BE000700F7 /* Webhook.swift in Sources */ = {isa = PBXBuildFile; fileRef = B3B7E52824EA579600A3FBD1 /* Webhook.swift */; };
B38BB6AF24EBB4D8000700F7 /* Configuration.swift in Sources */ = {isa = PBXBuildFile; fileRef = B36C8D7E24EA655900C00A99 /* Configuration.swift */; };
B39A65E01CC1FB1B003E26B0 /* PluginManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = B39A65DF1CC1FB1B003E26B0 /* PluginManager.swift */; };
PM00000000000000000001BF /* PluginManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = B39A65DF1CC1FB1B003E26B0 /* PluginManager.swift */; };
B39F2B391CB3BCA700C0D35C /* SendText.scpt in Resources */ = {isa = PBXBuildFile; fileRef = B39F2B381CB3BCA700C0D35C /* SendText.scpt */; };
B3A4C61423B867E300B7F009 /* MessageRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = B3A4C61323B867E300B7F009 /* MessageRequest.swift */; };
B3AECB5F2207CE33003E32F8 /* JaredFramework.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 7F84B61F1CC506790059A82B /* JaredFramework.framework */; };
Expand Down Expand Up @@ -204,6 +208,9 @@
B319236324DE76A2001904BA /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
B32DE51424D51F9C00AB3A71 /* RouterTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RouterTests.swift; sourceTree = "<group>"; };
EC71CM00000000000000A001 /* CoreModuleTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CoreModuleTests.swift; sourceTree = "<group>"; };
RT00TEST0000000000001FRF /* RouteTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RouteTests.swift; sourceTree = "<group>"; };
WRF0TEST0000000000001FRF /* WebhookRouteFactoryTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WebhookRouteFactoryTests.swift; sourceTree = "<group>"; };
PM00TEST0000000000001FRF /* PluginManagerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PluginManagerTests.swift; sourceTree = "<group>"; };
B32DE51924D5524300AB3A71 /* Action.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Action.swift; sourceTree = "<group>"; };
B32DE51B24D552D300AB3A71 /* MockPluginManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockPluginManager.swift; sourceTree = "<group>"; };
B33278511CB2C94700BBDD3A /* AppKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = AppKit.framework; path = System/Library/Frameworks/AppKit.framework; sourceTree = SDKROOT; };
Expand Down Expand Up @@ -409,6 +416,9 @@
B3B844B924D77A4800DC162A /* SendStyleTest.swift */,
B32DE51424D51F9C00AB3A71 /* RouterTests.swift */,
EC71CM00000000000000A001 /* CoreModuleTests.swift */,
RT00TEST0000000000001FRF /* RouteTests.swift */,
WRF0TEST0000000000001FRF /* WebhookRouteFactoryTests.swift */,
PM00TEST0000000000001FRF /* PluginManagerTests.swift */,
EC70PT00000000000000A001 /* InboundFilterPolicyTests.swift */,
B3B844CF24DA6AC800DC162A /* JaredWebServerTest.swift */,
TM0000010000000000000005 /* TunnelManagerTests.swift */,
Expand Down Expand Up @@ -882,6 +892,7 @@
B3B844C724D9298000DC162A /* MockRouter.swift in Sources */,
B347A4AD24DD170C00E93657 /* ActionTest.swift in Sources */,
B36710E224DFB5EC00DA1C5D /* JaredMock.swift in Sources */,
PM00000000000000000001BF /* PluginManager.swift in Sources */,
B3B844C224D8FA7D00DC162A /* DatabaseTestHelper.swift in Sources */,
B38BB6AD24EB99BE000700F7 /* Webhook.swift in Sources */,
AD456B8A60A74A9E90DC26CA /* KeychainAccessor.swift in Sources */,
Expand All @@ -899,6 +910,9 @@
TM0000010000000000000002 /* TunnelManager.swift in Sources */,
B32DE51524D51F9C00AB3A71 /* RouterTests.swift in Sources */,
EC71CM00000000000000A002 /* CoreModuleTests.swift in Sources */,
RT00TEST0000000000002BF0 /* RouteTests.swift in Sources */,
WRF0TEST0000000000002BF0 /* WebhookRouteFactoryTests.swift in Sources */,
PM00TEST0000000000002BF0 /* PluginManagerTests.swift in Sources */,
EC71C000000000000000A010 /* CoreModule.swift in Sources */,
EC71CS00000000000000A004 /* ContactNameService.swift in Sources */,
EC71CK00000000000000A004 /* Clock.swift in Sources */,
Expand Down
76 changes: 76 additions & 0 deletions JaredTests/PluginManagerTests.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
import XCTest
import JaredFramework
@testable import Jared

class PluginManagerTests: XCTestCase {

private func makeManager(disabledCommands: [String: Bool] = [:]) -> PluginManager {
let config = ConfigurationFile(disabledCommands: disabledCommands)
return PluginManager(sender: JaredMock(), configuration: config)
}

// MARK: - enabled()

func testEnabledReturnsTrueWhenCommandNotInMap() {
XCTAssertTrue(makeManager().enabled(routeName: "/ping"))
}

func testEnabledReturnsFalseWhenCommandDisabled() {
XCTAssertFalse(makeManager(disabledCommands: ["/ping": true]).enabled(routeName: "/ping"))
}

func testEnabledReturnsTrueWhenCommandExplicitlyEnabled() {
XCTAssertTrue(makeManager(disabledCommands: ["/ping": false]).enabled(routeName: "/ping"))
}

func testEnabledIsCaseInsensitive() {
XCTAssertFalse(makeManager(disabledCommands: ["/ping": true]).enabled(routeName: "/PING"))
}

// MARK: - getAllModules() / getAllRoutes()

func testGetAllModulesHasFourInternalModulesOnInit() {
// CoreModule, ScheduleModule, InternalModule, WebHookManager
XCTAssertEqual(makeManager().getAllModules().count, 4)
}

func testGetAllRoutesIncludesPingRoute() {
XCTAssertTrue(makeManager().getAllRoutes().contains { $0.name == "/ping" })
}

// MARK: - reload()

func testReloadPreservesModuleCount() {
let manager = makeManager()
manager.reload()
XCTAssertEqual(manager.getAllModules().count, 4)
}

func testReloadRestoresPingRoute() {
let manager = makeManager()
manager.reload()
XCTAssertTrue(manager.getAllRoutes().contains { $0.name == "/ping" })
}

// MARK: - apply()

func testApplyUpdatesDisabledCommands() {
let manager = makeManager()
XCTAssertTrue(manager.enabled(routeName: "/ping"))

manager.apply(ConfigurationFile(disabledCommands: ["/ping": true]))

XCTAssertFalse(manager.enabled(routeName: "/ping"))
}

func testApplyUpdatesWebhooks() {
let manager = makeManager()
XCTAssertTrue(manager.config.webhooks.isEmpty)

let hook = RichWebhook(url: "https://example.com")
manager.apply(ConfigurationFile(webhooks: [hook]))

XCTAssertEqual(manager.config.webhooks.count, 1)
XCTAssertEqual(manager.config.webhooks.first?.url, "https://example.com")
}
}
87 changes: 87 additions & 0 deletions JaredTests/RouteTests.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
import XCTest
import JaredFramework

class RouteTests: XCTestCase {

// MARK: - Compare raw values

func testCompareRawValues() {
XCTAssertEqual(Compare.startsWith.rawValue, "startsWith")
XCTAssertEqual(Compare.contains.rawValue, "contains")
XCTAssertEqual(Compare.is.rawValue, "is")
XCTAssertEqual(Compare.containsURL.rawValue, "containsURL")
XCTAssertEqual(Compare.isReaction.rawValue, "isReaction")
}

// MARK: - Route init

func testRouteInitMinimalPreservesProperties() {
var called = false
let route = Route(name: "ping", comparisons: [.startsWith: ["/ping"]], call: { _ in called = true })
XCTAssertEqual(route.name, "ping")
XCTAssertEqual(route.comparisons[.startsWith], ["/ping"])
XCTAssertNil(route.description)
XCTAssertNil(route.parameterSyntax)
}

func testRouteInitWithDescriptionPreservesIt() {
let route = Route(name: "r", comparisons: [.contains: ["foo"]], call: { _ in }, description: "does foo")
XCTAssertEqual(route.description, "does foo")
XCTAssertNil(route.parameterSyntax)
}

func testRouteInitWithSyntaxPreservesIt() {
let route = Route(name: "r", comparisons: [:], call: { _ in }, description: "d", parameterSyntax: "/r,arg")
XCTAssertEqual(route.parameterSyntax, "/r,arg")
}

// MARK: - JSON decoding

func testRouteDecodesFromJSON() throws {
let json = """
{
"name": "ping",
"description": "Replies pong",
"parameterSyntax": "/ping",
"comparisons": { "startsWith": ["/ping"] }
}
""".data(using: .utf8)!

let route = try JSONDecoder().decode(Route.self, from: json)

XCTAssertEqual(route.name, "ping")
XCTAssertEqual(route.description, "Replies pong")
XCTAssertEqual(route.parameterSyntax, "/ping")
XCTAssertEqual(route.comparisons[.startsWith], ["/ping"])
}

func testRouteDecodesMultipleComparisons() throws {
let json = """
{
"name": "multi",
"description": "d",
"parameterSyntax": "/m",
"comparisons": { "startsWith": ["/m"], "contains": ["foo", "bar"] }
}
""".data(using: .utf8)!

let route = try JSONDecoder().decode(Route.self, from: json)

XCTAssertEqual(route.comparisons[.startsWith], ["/m"])
XCTAssertEqual(route.comparisons[.contains], ["foo", "bar"])
}

func testCompareDecoderThrowsOnUnknownKey() {
let json = """
{
"name": "test",
"description": "test",
"parameterSyntax": "/test",
"comparisons": { "unknownCompareType": ["/test"] }
}
""".data(using: .utf8)!

XCTAssertThrowsError(try JSONDecoder().decode(Route.self, from: json),
"unknown Compare key must throw DecodingError")
}
}
91 changes: 91 additions & 0 deletions JaredTests/TunnelManagerTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,13 @@ final class MockTunnelRunner: TunnelRunner {
var stopCallCount = 0
var onOutput: ((String) -> Void)?

var onComplete: ((Error?) -> Void)?

func start(command: TunnelLaunchCommand, onOutput: @escaping (String) -> Void, onComplete: @escaping (Error?) -> Void) {
startCallCount += 1
lastCommand = command
self.onOutput = onOutput
self.onComplete = onComplete
isRunning = true
}

Expand Down Expand Up @@ -162,6 +165,94 @@ class TunnelManagerTests: XCTestCase {
XCTAssertGreaterThanOrEqual(runner.stopCallCount, 1)
}

func testSyncStartsNgrokWithTokenFromKeychain() {
let runner = MockTunnelRunner()
let keychain = MockKeychain()
keychain.save(secret: "mytoken", for: TunnelManager.ngrokKeychainAccount)
let manager = TunnelManager(
configuration: TunnelConfiguration(enabled: true, provider: .ngrok),
runner: runner,
keychain: keychain,
localPortProvider: { 3005 }
)

manager.sync(restApiEnabled: true, localPort: 3005)

XCTAssertEqual(runner.startCallCount, 1)
XCTAssertEqual(runner.lastCommand, .ngrok(localPort: 3005, authToken: "mytoken"))
}

func testSyncSetsErrorWhenNgrokTokenMissing() {
let runner = MockTunnelRunner()
let manager = TunnelManager(
configuration: TunnelConfiguration(enabled: true, provider: .ngrok),
runner: runner,
keychain: MockKeychain(),
localPortProvider: { 3005 }
)

manager.sync(restApiEnabled: true, localPort: 3005)

XCTAssertEqual(runner.startCallCount, 0)
XCTAssertNotNil(manager.lastError)
}

func testSyncSetsErrorWhenNgrokTokenIsEmpty() {
let runner = MockTunnelRunner()
let keychain = MockKeychain()
keychain.save(secret: "", for: TunnelManager.ngrokKeychainAccount)
let manager = TunnelManager(
configuration: TunnelConfiguration(enabled: true, provider: .ngrok),
runner: runner,
keychain: keychain,
localPortProvider: { 3005 }
)

manager.sync(restApiEnabled: true, localPort: 3005)

XCTAssertEqual(runner.startCallCount, 0)
XCTAssertNotNil(manager.lastError)
}

func testProcessErrorSetsLastError() {
let runner = MockTunnelRunner()
let manager = TunnelManager(
configuration: TunnelConfiguration(enabled: true, provider: .cloudflared),
runner: runner,
keychain: MockKeychain(),
localPortProvider: { 3005 }
)

manager.sync(restApiEnabled: true, localPort: 3005)
runner.onComplete?(TunnelError.processExited(127))

XCTAssertNotNil(manager.lastError)
XCTAssertTrue(manager.lastError!.contains("127"), "error message should include exit code")
}

func testDuplicateURLLineDoesNotPostSecondNotification() {
let runner = MockTunnelRunner()
let manager = TunnelManager(
configuration: TunnelConfiguration(enabled: true, provider: .cloudflared),
runner: runner,
keychain: MockKeychain(),
localPortProvider: { 3005 }
)
var notificationCount = 0
let token = NotificationCenter.default.addObserver(
forName: TunnelManager.publicURLDidChangeNotification,
object: manager,
queue: nil
) { _ in notificationCount += 1 }

manager.sync(restApiEnabled: true, localPort: 3005)
runner.onOutput?("| https://abc-def.trycloudflare.com |")
runner.onOutput?("| https://abc-def.trycloudflare.com |")

NotificationCenter.default.removeObserver(token)
XCTAssertEqual(notificationCount, 1, "duplicate URL line must not post a second notification")
}

func testWebserverConfigurationDecodesTunnelBlock() throws {
let json = """
{
Expand Down
55 changes: 55 additions & 0 deletions JaredTests/WebhookRouteFactoryTests.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import XCTest
import JaredFramework
@testable import Jared

class WebhookRouteFactoryTests: XCTestCase {

private func makeFactory() -> WebhookRouteFactory {
let dir = URL(fileURLWithPath: NSTemporaryDirectory())
.appendingPathComponent("WebhookRouteFactoryTests-\(UUID().uuidString)", isDirectory: true)
let client = WebhookDeliveryClient(
session: .ephemeral,
keychain: MockKeychain(),
sender: nil,
deliveryStore: WebhookDeliveryStore(fileURL: dir.appendingPathComponent("deliveries.json"))
)
return WebhookRouteFactory(client: client)
}

func testRoutesFromWebhookWithNilRoutesReturnsEmpty() {
let webhook = RichWebhook(url: "https://example.com", routes: nil)
XCTAssertTrue(makeFactory().routes(from: webhook).isEmpty)
}

func testRoutesFromWebhookWithEmptyRoutesReturnsEmpty() {
let webhook = RichWebhook(url: "https://example.com", routes: [])
XCTAssertTrue(makeFactory().routes(from: webhook).isEmpty)
}

func testRoutesPreserveNamesAndComparisons() {
let r1 = Route(name: "greet", comparisons: [.startsWith: ["/hello"]], call: { _ in })
let r2 = Route(name: "bye", comparisons: [.is: ["/bye"]], call: { _ in })
let webhook = RichWebhook(url: "https://example.com", routes: [r1, r2])

let produced = makeFactory().routes(from: webhook)

XCTAssertEqual(produced.count, 2)
XCTAssertEqual(produced[0].name, "greet")
XCTAssertEqual(produced[0].comparisons[.startsWith], ["/hello"])
XCTAssertEqual(produced[1].name, "bye")
XCTAssertEqual(produced[1].comparisons[.is], ["/bye"])
}

func testEachRouteGetsFreshDeliveryCallClosure() {
let r1 = Route(name: "a", comparisons: [.startsWith: ["/a"]], call: { _ in })
let r2 = Route(name: "b", comparisons: [.startsWith: ["/b"]], call: { _ in })
let webhook = RichWebhook(url: "https://example.com", routes: [r1, r2])

let produced = makeFactory().routes(from: webhook)

// Each route's call is wired by the factory (not the original stub closure)
XCTAssertEqual(produced.count, 2)
XCTAssertNotNil(produced[0].call as Any)
XCTAssertNotNil(produced[1].call as Any)
}
}
Loading