167 lines
9.2 KiB
Swift
167 lines
9.2 KiB
Swift
import XCTest
|
|
import WebKit
|
|
|
|
final class WorkspaceTests: XCTestCase {
|
|
func testServerValidationAndOriginBoundary() throws {
|
|
let url = try XCTUnwrap(WorkspaceServer.normalize(" HTTPS://Example.com:443/kanban "))
|
|
XCTAssertEqual(url.absoluteString, "https://example.com/kanban/")
|
|
let server = WorkspaceServer(url: url, profile: UUID())
|
|
for value in ["http://example.com/", "https://user:secret@example.com/", "https://example.com/?token=1", "https://example.com/#x", "https://exa\nmple.com", "https://example.com\\evil", "file:///etc/passwd", "javascript:alert(1)", "https://example.com:99999/"] {
|
|
XCTAssertNil(WorkspaceServer.normalize(value), value)
|
|
}
|
|
for value in ["https://example.com/login", "https://example.com/two-factor-challenge", "https://example.com/projects/1?ticket=54#details"] {
|
|
XCTAssertTrue(server.allows(try XCTUnwrap(URL(string: value))), value)
|
|
}
|
|
for value in ["http://example.com/", "https://example.com:8443/", "https://evil.example.com/", "https://user@example.com/", "file:///tmp/index.html", "tauri://localhost/action/open"] {
|
|
XCTAssertFalse(server.allows(try XCTUnwrap(URL(string: value))), value)
|
|
}
|
|
}
|
|
|
|
func testExternalLinksStayOutsideTheSelectedServer() throws {
|
|
let server = WorkspaceServer(url: URL(string: "https://example.com/")!, profile: UUID())
|
|
for value in ["https://git.example.com/org/repo/pulls/1#diff", "http://git.example.com/repo", "https://example.com:8443/other"] {
|
|
let url = try XCTUnwrap(URL(string: value))
|
|
XCTAssertTrue(WorkspaceServer.allowsExternal(url))
|
|
XCTAssertFalse(server.allows(url))
|
|
}
|
|
for value in ["https://user:secret@git.example.com/", "https://example.com:99999/", "javascript:alert(1)", "file:///etc/passwd", "tauri://localhost/action/open", "mailto:user@example.com", "data:text/html,test"] {
|
|
XCTAssertFalse(WorkspaceServer.allowsExternal(try XCTUnwrap(URL(string: value))), value)
|
|
}
|
|
}
|
|
|
|
@MainActor
|
|
func testBackButtonTracksSameDocumentHistory() async throws {
|
|
let name = "KAN55-history-\(UUID())"
|
|
let defaults = try XCTUnwrap(UserDefaults(suiteName: name))
|
|
defer { defaults.removePersistentDomain(forName: name) }
|
|
let controller = WorkspaceViewController(defaults: defaults)
|
|
controller.loadViewIfNeeded()
|
|
let url = try XCTUnwrap(WorkspaceServer.normalize("https://example.invalid/"))
|
|
controller.open(url)
|
|
let webview = try XCTUnwrap(controller.workspace)
|
|
webview.stopLoading()
|
|
webview.loadHTMLString("<html><body><input id='draft' value='unsaved'></body></html>", baseURL: url)
|
|
var loaded = false
|
|
for _ in 0..<50 {
|
|
if let value = try? await webview.evaluateJavaScript("document.getElementById('draft')?.value"),
|
|
value as? String == "unsaved" { loaded = true; break }
|
|
try await Task.sleep(for: .milliseconds(100))
|
|
}
|
|
XCTAssertTrue(loaded)
|
|
_ = try await webview.evaluateJavaScript("history.pushState({}, '', '#ticket')")
|
|
let toolbar = try XCTUnwrap(controller.view.subviews.compactMap { $0 as? UIToolbar }.first)
|
|
for _ in 0..<20 {
|
|
if toolbar.items?.first?.isEnabled == true { break }
|
|
try await Task.sleep(for: .milliseconds(50))
|
|
}
|
|
XCTAssertTrue(webview.canGoBack)
|
|
XCTAssertTrue(toolbar.items?.first?.isEnabled == true)
|
|
webview.goBack()
|
|
for _ in 0..<20 {
|
|
if let hash = try? await webview.evaluateJavaScript("location.hash"), hash as? String == "" { break }
|
|
try await Task.sleep(for: .milliseconds(50))
|
|
}
|
|
let draft = try await webview.evaluateJavaScript("document.getElementById('draft').value")
|
|
XCTAssertEqual(draft as? String, "unsaved")
|
|
}
|
|
|
|
func testServerChangesNeverReusePreviousProfile() throws {
|
|
let firstURL = try XCTUnwrap(WorkspaceServer.normalize("https://example.com/"))
|
|
let otherURL = try XCTUnwrap(WorkspaceServer.normalize("https://example.com/other/"))
|
|
let first = WorkspaceServer.selecting(firstURL, current: nil)
|
|
XCTAssertEqual(WorkspaceServer.selecting(firstURL, current: first), first)
|
|
let second = WorkspaceServer.selecting(otherURL, current: first)
|
|
XCTAssertNotEqual(second.profile, first.profile)
|
|
XCTAssertNotEqual(WorkspaceServer.selecting(firstURL, current: second).profile, first.profile)
|
|
XCTAssertEqual(try JSONDecoder().decode(WorkspaceServer.self, from: JSONEncoder().encode(first)), first)
|
|
}
|
|
|
|
func testTimeoutDoesNotDiscardLoadedWorkspaceOrAffectLaterNavigation() {
|
|
var state = WorkspaceConnection()
|
|
let first = state.begin()
|
|
let second = state.begin()
|
|
XCTAssertFalse(state.fail(first))
|
|
state.finish()
|
|
XCTAssertFalse(state.fail(second))
|
|
let third = state.begin()
|
|
XCTAssertTrue(state.fail(third))
|
|
XCTAssertTrue(state.ready)
|
|
XCTAssertFalse(state.fail(third))
|
|
}
|
|
|
|
@MainActor
|
|
func testWKWebViewPersistsCookiesAcrossViewsAndIsolatesServerChanges() async throws {
|
|
let profile = UUID()
|
|
let first = WKWebViewConfiguration()
|
|
first.websiteDataStore = WKWebsiteDataStore(forIdentifier: profile)
|
|
let cookie = try XCTUnwrap(HTTPCookie(properties: [
|
|
.domain: "example.com", .path: "/", .name: "laravel_session", .value: "test-session",
|
|
.secure: "TRUE", .expires: Date().addingTimeInterval(3600),
|
|
]))
|
|
await first.websiteDataStore.httpCookieStore.setCookie(cookie)
|
|
let restored = WKWebViewConfiguration()
|
|
restored.websiteDataStore = WKWebsiteDataStore(forIdentifier: profile)
|
|
let restoredCookies = await restored.websiteDataStore.httpCookieStore.allCookies()
|
|
XCTAssertTrue(restoredCookies.contains { $0.name == "laravel_session" && $0.value == "test-session" })
|
|
let other = WKWebsiteDataStore(forIdentifier: UUID())
|
|
let otherCookies = await other.httpCookieStore.allCookies()
|
|
XCTAssertTrue(otherCookies.isEmpty)
|
|
await restored.websiteDataStore.httpCookieStore.deleteCookie(cookie)
|
|
let loggedOut = await first.websiteDataStore.httpCookieStore.allCookies()
|
|
XCTAssertFalse(loggedOut.contains { $0.name == "laravel_session" })
|
|
}
|
|
|
|
@MainActor
|
|
func testControllerRestoresServerAndCreatesIsolatedUnprivilegedWebView() throws {
|
|
let name = "KAN54-tests-\(UUID())"
|
|
let defaults = try XCTUnwrap(UserDefaults(suiteName: name))
|
|
defer { defaults.removePersistentDomain(forName: name) }
|
|
let controller = WorkspaceViewController(defaults: defaults)
|
|
controller.loadViewIfNeeded()
|
|
let url = try XCTUnwrap(WorkspaceServer.normalize("https://example.invalid/"))
|
|
controller.open(url)
|
|
let server = try XCTUnwrap(controller.server)
|
|
XCTAssertEqual(controller.workspace?.configuration.websiteDataStore.identifier, server.profile)
|
|
XCTAssertTrue(controller.workspace?.configuration.userContentController.userScripts.isEmpty == true)
|
|
let restored = WorkspaceViewController(defaults: defaults)
|
|
restored.loadViewIfNeeded()
|
|
XCTAssertEqual(restored.server, server)
|
|
let other = try XCTUnwrap(WorkspaceServer.normalize("https://other.invalid/"))
|
|
restored.open(other)
|
|
XCTAssertNotEqual(restored.server?.profile, server.profile)
|
|
}
|
|
@MainActor
|
|
func testConnectionFailureKeepsLoadedFormAndAllowsResume() async throws {
|
|
let name = "KAN54-form-\(UUID())"
|
|
let defaults = try XCTUnwrap(UserDefaults(suiteName: name))
|
|
defer { defaults.removePersistentDomain(forName: name) }
|
|
let controller = WorkspaceViewController(defaults: defaults)
|
|
controller.loadViewIfNeeded()
|
|
let url = try XCTUnwrap(WorkspaceServer.normalize("https://example.invalid/"))
|
|
controller.open(url)
|
|
let webview = try XCTUnwrap(controller.workspace)
|
|
webview.stopLoading()
|
|
let navigation = try XCTUnwrap(webview.loadHTMLString("<html><body><input id='draft' value='unsaved ticket'></body></html>", baseURL: url))
|
|
var loaded = false
|
|
for _ in 0..<50 {
|
|
if let value = try? await webview.evaluateJavaScript("document.getElementById('draft')?.value"),
|
|
value as? String == "unsaved ticket" { loaded = true; break }
|
|
try await Task.sleep(for: .milliseconds(100))
|
|
}
|
|
XCTAssertTrue(loaded)
|
|
controller.webView(webview, didFinish: navigation)
|
|
controller.webView(webview, didStartProvisionalNavigation: navigation)
|
|
controller.webView(webview, didFailProvisionalNavigation: navigation,
|
|
withError: NSError(domain: NSURLErrorDomain, code: NSURLErrorNotConnectedToInternet))
|
|
XCTAssertTrue(webview.isHidden)
|
|
controller.open(url)
|
|
XCTAssertTrue(controller.workspace === webview)
|
|
XCTAssertFalse(webview.isHidden)
|
|
let value = try await webview.evaluateJavaScript("document.getElementById('draft').value")
|
|
XCTAssertEqual(value as? String, "unsaved ticket")
|
|
let bridge = try await webview.evaluateJavaScript("typeof window.__TAURI_INTERNALS__")
|
|
XCTAssertEqual(bridge as? String, "undefined")
|
|
}
|
|
|
|
}
|