mirror of
https://github.com/Novaloop-AG/element-x-ios.git
synced 2026-11-02 12:07:55 +00:00
Add HealthChat API and extended profile services (#9)
Expose OpenID tokens and homeserver credentials from the client proxy, and add fork-only services for the HealthChat member directory backend (api.healthchat.ch) plus a service that reads MSC4133 extended profile fields directly from the homeserver. Groundwork for the members tab and the profile screens. Claude-Session: https://claude.ai/code/session_013qdTbAWsZj2quQzEkagXaS
This commit is contained in:
@@ -0,0 +1,117 @@
|
||||
//
|
||||
// Copyright 2026 Element Creations Ltd.
|
||||
//
|
||||
// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
|
||||
// Please see LICENSE files in the repository root for full details.
|
||||
//
|
||||
|
||||
@testable import ElementX
|
||||
import Foundation
|
||||
import Synchronization
|
||||
import Testing
|
||||
|
||||
final class ExtendedProfileServiceTests {
|
||||
private let clientProxy = ClientProxyMock(.init())
|
||||
private let service: ExtendedProfileService
|
||||
|
||||
init() {
|
||||
let configuration = URLSessionConfiguration.ephemeral
|
||||
configuration.protocolClasses = [MockProfileURLProtocol.self]
|
||||
service = ExtendedProfileService(clientProxy: clientProxy,
|
||||
urlSession: URLSession(configuration: configuration))
|
||||
|
||||
clientProxy.homeserverCredentialsReturnValue = .success(.init(homeserverURL: "https://matrix.example.com",
|
||||
accessToken: MockProfileURLProtocol.accessToken))
|
||||
}
|
||||
|
||||
@Test
|
||||
func fetchesProfileWithBearerToken() async throws {
|
||||
let result = await service.profile(for: "@alice:healthchat.ch")
|
||||
|
||||
let profile = try result.get()
|
||||
#expect(profile.displayName == "Dr. Alice Alpha")
|
||||
#expect(profile.businessTel == "+41 44 123 45 67")
|
||||
|
||||
let request = try #require(MockProfileURLProtocol.lastRequest)
|
||||
#expect(request.url?.path(percentEncoded: false) == "/_matrix/client/v3/profile/@alice:healthchat.ch")
|
||||
#expect(request.value(forHTTPHeaderField: "Authorization") == "Bearer \(MockProfileURLProtocol.accessToken)")
|
||||
}
|
||||
|
||||
@Test
|
||||
func missingCredentialsFailWithoutRequest() async {
|
||||
clientProxy.homeserverCredentialsReturnValue = .failure(.sdkError(ClientProxyMockError.generic))
|
||||
|
||||
let result = await service.profile(for: "@alice:healthchat.ch")
|
||||
|
||||
#expect(result == .failure(.missingCredentials))
|
||||
}
|
||||
|
||||
@Test
|
||||
func unknownUserFailsWithHTTPStatus() async {
|
||||
let result = await service.profile(for: "@unknown:healthchat.ch")
|
||||
|
||||
#expect(result == .failure(.httpStatus(404)))
|
||||
}
|
||||
|
||||
@Test
|
||||
func credentialsDescriptionRedactsAccessToken() {
|
||||
let credentials = HomeserverCredentials(homeserverURL: "https://matrix.example.com",
|
||||
accessToken: "supersecret")
|
||||
|
||||
#expect(!String(describing: credentials).contains("supersecret"))
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - MockProfileURLProtocol
|
||||
|
||||
private nonisolated class MockProfileURLProtocol: URLProtocol {
|
||||
static let accessToken = "testAccessToken"
|
||||
|
||||
private static let _lastRequest: Mutex<URLRequest?> = .init(nil)
|
||||
static var lastRequest: URLRequest? {
|
||||
_lastRequest.withLock { $0 }
|
||||
}
|
||||
|
||||
private static let profileJSON = """
|
||||
{
|
||||
"displayname": "Dr. Alice Alpha",
|
||||
"io.element.profile.business_tel": "+41 44 123 45 67"
|
||||
}
|
||||
"""
|
||||
|
||||
private static let responses: [String: (Int, Data)] = [
|
||||
"/_matrix/client/v3/profile/@alice:healthchat.ch": (200, Data(profileJSON.utf8))
|
||||
]
|
||||
|
||||
override func startLoading() {
|
||||
MockProfileURLProtocol._lastRequest.withLock { $0 = request }
|
||||
|
||||
guard let url = request.url else {
|
||||
client?.urlProtocol(self, didFailWithError: URLError(.badURL))
|
||||
return
|
||||
}
|
||||
|
||||
let isAuthenticated = request.value(forHTTPHeaderField: "Authorization") == "Bearer \(MockProfileURLProtocol.accessToken)"
|
||||
let path = url.path(percentEncoded: false)
|
||||
let (statusCode, data) = isAuthenticated ? MockProfileURLProtocol.responses[path] ?? (404, Data()) : (401, Data())
|
||||
|
||||
guard let response = HTTPURLResponse(url: url, statusCode: statusCode, httpVersion: nil, headerFields: nil) else {
|
||||
client?.urlProtocol(self, didFailWithError: URLError(.badServerResponse))
|
||||
return
|
||||
}
|
||||
|
||||
client?.urlProtocol(self, didReceive: response, cacheStoragePolicy: .notAllowed)
|
||||
client?.urlProtocol(self, didLoad: data)
|
||||
client?.urlProtocolDidFinishLoading(self)
|
||||
}
|
||||
|
||||
override func stopLoading() { }
|
||||
|
||||
override class func canonicalRequest(for request: URLRequest) -> URLRequest {
|
||||
request
|
||||
}
|
||||
|
||||
override class func canInit(with request: URLRequest) -> Bool {
|
||||
true
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user