aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorFinn Behrens <me@kloenk.dev>2021-09-29 09:49:57 +0200
committerFinn Behrens <me@kloenk.dev>2021-09-29 09:49:57 +0200
commit5ded103c98e35dbfe450099b3be7f53f177e221a (patch)
treee447123eb85c39e2fe5668b18e5b753320e5a330
parentcd30721025c174cfaced64513ada8999f628168b (diff)
downloadMatrixCore-5ded103c98e35dbfe450099b3be7f53f177e221a.tar.gz
MatrixCore-5ded103c98e35dbfe450099b3be7f53f177e221a.tar.xz
MatrixCore-5ded103c98e35dbfe450099b3be7f53f177e221a.zip
Well Known and Login flows
Signed-off-by: Finn Behrens <me@kloenk.dev>
-rw-r--r--Package.resolved16
-rw-r--r--Package.swift15
-rw-r--r--Sources/MatrixClient/Error.swift64
-rw-r--r--Sources/MatrixClient/HomeServer.swift118
-rw-r--r--Sources/MatrixClient/Login.swift242
-rw-r--r--Sources/MatrixClient/MatrixClient.swift120
-rw-r--r--Tests/MatrixClientTests/ClientTests.swift63
7 files changed, 638 insertions, 0 deletions
diff --git a/Package.resolved b/Package.resolved
new file mode 100644
index 0000000..7309574
--- /dev/null
+++ b/Package.resolved
@@ -0,0 +1,16 @@
+{
+ "object": {
+ "pins": [
+ {
+ "package": "AnyCodable",
+ "repositoryURL": "https://github.com/Flight-School/AnyCodable",
+ "state": {
+ "branch": null,
+ "revision": "b1a7a8a6186f2fcb28f7bda67cfc545de48b3c80",
+ "version": "0.6.2"
+ }
+ }
+ ]
+ },
+ "version": 1
+}
diff --git a/Package.swift b/Package.swift
index d245f47..f83580c 100644
--- a/Package.swift
+++ b/Package.swift
@@ -5,20 +5,35 @@ import PackageDescription
let package = Package(
name: "MatrixCore",
+ platforms: [
+ .iOS(.v15), .tvOS(.v15),
+ .watchOS(.v8), .macOS(.v12), .macCatalyst(.v15)
+ ],
products: [
// Products define the executables and libraries a package produces, and make them visible to other packages.
.library(
+ name: "MatrixClient",
+ targets: ["MatrixClient"]),
+ .library(
name: "MatrixCore",
targets: ["MatrixCore"]),
],
dependencies: [
// Dependencies declare other packages that this package depends on.
// .package(url: /* package url */, from: "1.0.0"),
+ .package(name: "AnyCodable", url: "https://github.com/Flight-School/AnyCodable", from: "0.6.0")
],
targets: [
// Targets are the basic building blocks of a package. A target can define a module or a test suite.
// Targets can depend on other targets in this package, and on products in packages this package depends on.
.target(
+ name: "MatrixClient",
+ dependencies: ["AnyCodable"]),
+ .testTarget(
+ name: "MatrixClientTests",
+ dependencies: ["MatrixClient"]),
+
+ .target(
name: "MatrixCore",
dependencies: []),
.testTarget(
diff --git a/Sources/MatrixClient/Error.swift b/Sources/MatrixClient/Error.swift
new file mode 100644
index 0000000..0998788
--- /dev/null
+++ b/Sources/MatrixClient/Error.swift
@@ -0,0 +1,64 @@
+//
+// Error.swift
+// Error
+//
+// Created by Finn Behrens on 07.08.21.
+//
+
+import Foundation
+
+public enum MatrixError: String, Error, Codable {
+ case Forbidden = "M_FORBIDDEN"
+ case Unknown = "M_UNKNOWN"
+ case UnknownToken = "M_UNKNOWN_TOKEN"
+ case BadJSON = "M_BAD_JSON"
+ case NotFound = "M_NOT_FOUND"
+ case LimitExceeded = "M_LIMIT_EXCEEDED"
+ case UserInUse = "M_USER_IN_USE"
+ case RoomInUse = "M_ROOM_IN_USE"
+ case BadPagination = "M_BAD_PAGINATON"
+ case Unauthorized = "M_UNAUTHORIZED"
+ case OldVersion = "M_OLD_VERSION"
+ case Unrecognized = "M_UNRECOGNIZED"
+ case LoginEmailURLNotYet = "M_LOGIN_EMAIL_URL_NOT_YET"
+ case ThreePIDAuthFailed = "M_THREEPID_AUTH_FAILED"
+ case ThreePIDInUse = "M_THREEPID_IN_USE"
+ case ThreePIDNotFound = "M_THREEPID_NOT_FOUND"
+ case ServerNotTrusted = "M_SERVER_NOT_TRUSTED"
+ case GuestAccessforbidden = "M_GUEST_ACCESS_FORBIDDEN"
+ case ConsentNotGiven = "M_CONSENT_NOT_GIVEN"
+ case ResourceLimitExceeded = "M_RESOURCE_LIMIT_EXCEEDED"
+ case BackupWrongKeysVersion = "M_WRONG_ROOM_KEYS_VERSION"
+ case PasswordToShort = "M_PASSWORD_TOO_SHORT"
+ case PasswordNoDigit = "M_PASSWORD_NO_DIGIT"
+ case PasswordNoUppercase = "M_PASSWORD_NO_UPPERCASE"
+ case PasswordNoLowercase = "M_PASSWORD_NO_LOWERCASE"
+ case PasswordNoSymbol = "M_PASSWORD_NO_SYMBOL"
+ case PasswordInDictionary = "M_PASSWORD_IN_DICTIONARY"
+ case PasswordWeak = "M_WEAK_PASSWORD"
+ case TermsNotSigned = "M_TERMS_NOT_SIGNED"
+ case InvalidPepper = "M_INVALID_PEPPER"
+ case Exclusive = "M_EXCLUSIVE"
+
+}
+
+public struct MatrixServerError: Error, Codable {
+ /// Error code
+ public var errcode: MatrixError
+
+ /// Error message reported by the server
+ public var error: String
+
+ /// HTTP status code
+ public var code: Int?
+
+ // TODO: extra data
+
+ public init(json: Data, code: Int? = nil) throws {
+ let decoder = JSONDecoder()
+
+ self = try decoder.decode(Self.self, from: json)
+ self.code = code
+ }
+}
+
diff --git a/Sources/MatrixClient/HomeServer.swift b/Sources/MatrixClient/HomeServer.swift
new file mode 100644
index 0000000..64b44b0
--- /dev/null
+++ b/Sources/MatrixClient/HomeServer.swift
@@ -0,0 +1,118 @@
+//
+// HomeServer.swift
+// File
+//
+// Created by Finn Behrens on 27.09.21.
+//
+
+import Foundation
+import AnyCodable
+
+public struct MatrixHomeserver: Codable {
+ public var url: URLComponents
+
+ public init?(string: String) {
+ guard
+ let components = URLComponents(string: string),
+ components.host != nil
+ else {
+ return nil
+ }
+
+ self.url = components
+ }
+}
+
+public struct MatrixServerInfo: Codable {
+ /// The supported versions.
+ public var versions: [String]
+
+ /// Experimental features the server supports. Features not listed here, or the lack of this
+ /// property all together, indicate that a feature is not supported.
+ public var unstableFeatures: [String: Bool]?
+
+ enum CodingKeys: String, CodingKey {
+ case versions
+ case unstableFeatures = "unstable_features"
+ }
+}
+
+public struct MatrixWellKnown {
+ /// Used by clients to discover homeserver information.
+ public var homeserver: ServerInformation?
+
+ /// Used by clients to discover identity server information.
+ public var identityServer: ServerInformation?
+
+ // MARK: - dynamic variables
+ /// The base URL for the homeserver for client-server connections.
+ public var homeServerBaseUrl: String? {
+ homeserver?.baseURL
+ }
+
+ /// The base URL for the homeserver for client-server connections.
+ public var identityServerBaseUrl: String? {
+ identityServer?.baseURL
+ }
+
+ public var extraInfo: [String: AnyCodable]
+
+ public struct ServerInformation: Codable {
+ /// Base url of the server
+ public var baseURL: String
+
+ enum CodingKeys: String, CodingKey {
+ case baseURL = "base_url"
+ }
+ }
+}
+
+// TODO: encode for extraInfos
+extension MatrixWellKnown: Codable {
+ private enum KnownCodingKeys: String, CodingKey, CaseIterable {
+ case homeserver = "m.homeserver"
+ case identityServer = "m.identity_server"
+
+ static func doesNotContain(_ key: DynamicCodingKeys) -> Bool {
+ !Self.allCases.map(\.stringValue).contains(key.stringValue)
+ }
+ }
+
+ struct DynamicCodingKeys: CodingKey {
+ var stringValue: String
+ init?(stringValue: String) {
+ self.stringValue = stringValue
+ }
+
+ // not used here, but a protocol requirement
+ var intValue: Int?
+ init?(intValue: Int) {
+ return nil
+ }
+ }
+
+ public init(from decoder: Decoder) throws {
+ let container = try decoder.container(keyedBy: KnownCodingKeys.self)
+ self.homeserver = try container.decodeIfPresent(ServerInformation.self, forKey: .homeserver)
+ self.identityServer = try container.decodeIfPresent(ServerInformation.self, forKey: .identityServer)
+
+ self.extraInfo = [:]
+ let extraContainer = try decoder.container(keyedBy: DynamicCodingKeys.self)
+
+ for key in extraContainer.allKeys where KnownCodingKeys.doesNotContain(key) {
+ let decoded = try extraContainer.decode(AnyCodable.self, forKey: DynamicCodingKeys(stringValue: key.stringValue)!)
+ self.extraInfo[key.stringValue] = decoded
+ }
+ }
+
+ public func encode(to encoder: Encoder) throws {
+ var container = encoder.container(keyedBy: KnownCodingKeys.self)
+ try container.encodeIfPresent(self.homeserver, forKey: .homeserver)
+ try container.encodeIfPresent(self.identityServer, forKey: .identityServer)
+
+ var extraContainer = encoder.container(keyedBy: DynamicCodingKeys.self)
+ for (name, value) in self.extraInfo {
+ try extraContainer.encode(value, forKey: .init(stringValue: name)!)
+ }
+ }
+}
diff --git a/Sources/MatrixClient/Login.swift b/Sources/MatrixClient/Login.swift
new file mode 100644
index 0000000..fc70cdb
--- /dev/null
+++ b/Sources/MatrixClient/Login.swift
@@ -0,0 +1,242 @@
+//
+// File.swift
+// File
+//
+// Created by Finn Behrens on 27.09.21.
+//
+
+import Foundation
+
+public struct MatrixLoginFlow: Codable {
+ public var value: String
+
+ /// The client submits an identifier and secret password, both sent in plain-text.
+ ///
+ /// To use this authentication type, clients should submit an auth dict as follows:
+ /// ```json
+ /// {
+ /// "type": "m.login.password",
+ /// "identifier": {
+ /// ...
+ /// },
+ /// "password": "<password>",
+ /// "session": "<session ID>"
+ /// }
+ /// ```
+ ///
+ /// Alternatively reply using a 3PID bound to the user's account on the homeserver using the /account/3pid API rather then giving the user explicitly as follows:
+ /// ```json
+ /// {
+ /// "type": "m.login.password",
+ /// "identifier": {
+ /// "type": "m.id.thirdparty",
+ /// "medium": "<The medium of the third party identifier.>",
+ /// "address": "<The third party address of the user>"
+ /// },
+ /// "password": "<password>",
+ /// "session": "<session ID>"
+ /// }
+ /// ```
+ /// In the case that the homeserver does not know about the supplied 3PID, the homeserver must respond with 403 Forbidden.
+ public static let password: MatrixLoginFlow = "m.login.password"
+
+ /// The user completes a Google ReCaptcha 2.0 challenge
+ ///
+ /// To use this authentication type, clients should submit an auth dict as follows:
+ /// ```json
+ /// {
+ /// "type": "m.login.recaptcha",
+ /// "response": "<captcha response>",
+ /// "session": "<session ID>"
+ /// }
+ /// ```
+ public static let recaptcha: MatrixLoginFlow = "m.login.recaptcha"
+ public static let oauth2: MatrixLoginFlow = "m.login.oauth2"
+
+ /// Authentication is supported by authorising with an external single sign-on provider.
+ ///
+ /// A client wanting to complete authentication using SSO should use the Fallback authentication flow by opening a browser window for `/_matrix/client/r0/auth/m.login.sso/fallback/web?session=<...>` with the session parameter set to the session ID provided by the server.
+ /// The homeserver should return a page which asks for the user's confirmation before proceeding. For example, the page could say words to the effect of:
+ ///
+ /// A client is trying to remove a device/add an email address/take over your account. To confirm this action, re-authenticate with single sign-on. If you did not expect this, your account may be compromised!
+ ///
+ /// Once the user has confirmed they should be redirected to the single sign-on provider's login page. Once the provider has validated the user, the browser is redirected back to the homeserver.
+ ///
+ /// The homeserver then validates the response from the single sign-on provider and updates the user-interactive authentication session to mark the single sign-on stage has been completed. The browser is shown the fallback authentication completion page.
+ ///
+ /// Once the flow has completed, the client retries the request with the session only, as above.
+ public static let sso: MatrixLoginFlow = "m.login.sso"
+ public static let email: MatrixLoginFlow = "m.login.email.identity"
+ public static let msisdn: MatrixLoginFlow = "m.login.msisdn"
+ public static let token: MatrixLoginFlow = "m.login.token"
+ public static let dummy: MatrixLoginFlow = "m.login.dummy"
+
+ public init(from decoder: Decoder) throws {
+ let container = try decoder.singleValueContainer()
+ self.value = try container.decode(String.self)
+ }
+
+ public func encode(to encoder: Encoder) throws {
+ var container = encoder.singleValueContainer()
+ try container.encode(self.value)
+ }
+}
+
+extension MatrixLoginFlow: ExpressibleByStringLiteral {
+ public init(stringLiteral value: StringLiteralType) {
+ self.value = value
+ }
+}
+
+public enum MatrixLoginUserIdentifier: Codable {
+ /// 5.4.6.1 Matrix User id
+ ///
+ /// A client can identify a user using their Matrix ID. This can either be the fully qualified Matrix user ID, or just the localpart of the user ID.
+ ///
+ /// **Type**: `m.id.user`
+ case user(id: String)
+
+ /// 5.4.6.2 Third-party ID
+ ///
+ /// A client can identify a user using a 3PID associated with the user's account on the homeserver, where the 3PID was previously associated
+ /// using the /account/3pid API. See the 3PID Types Appendix for a list of Third-party ID media.
+ ///
+ /// **Type**: `m.id.thirdparty`
+ case thirdparty(medium: String, address: String)
+
+ /// 5.4.6.3 Phone number
+ ///
+ /// A client can identify a user using a phone number associated with the user's account, where the phone number was previously associated
+ /// using the /account/3pid API. The phone number can be passed in as entered by the user; the homeserver will be responsible for
+ /// canonicalising it. If the client wishes to canonicalise the phone number, then it can use the m.id.thirdparty identifier type with a medium o
+ /// msisdn instead.
+ ///
+ /// **Type**: `m.id.phone`
+ case phone(country: String, phone: String)
+
+ private enum CodingKeys: String, CodingKey {
+ case type
+ case user
+ case medium
+ case address
+ case country
+ case phone
+ }
+
+ enum MatrixLoginUserIdentifierError: Error {
+ case decodingInvalidType(String)
+ }
+
+ public init(from decoder: Decoder) throws {
+ let values = try decoder.container(keyedBy: CodingKeys.self)
+ let type = try values.decode(String.self, forKey: .type)
+
+ switch type {
+ case "m.id.user":
+ let id = try values.decode(String.self, forKey: .user)
+ self = .user(id: id)
+ return
+ case "m.id.thirdparty":
+ let medium = try values.decode(String.self, forKey: .medium)
+ let address = try values.decode(String.self, forKey: .address)
+ self = .thirdparty(medium: medium, address: address)
+ return
+ case "m.id.phone":
+ let country = try values.decode(String.self, forKey: .country)
+ let phone = try values.decode(String.self, forKey: .phone)
+ self = .phone(country: country, phone: phone)
+ return
+ default:
+ throw MatrixLoginUserIdentifierError.decodingInvalidType(type)
+ }
+ }
+
+ public func encode(to encoder: Encoder) throws {
+ var container = encoder.container(keyedBy: CodingKeys.self)
+ switch self {
+ case .user(id: let id):
+ try container.encode("m.id.user", forKey: .type)
+ try container.encode(id, forKey: .user)
+ case .thirdparty(medium: let medium, address: let address):
+ try container.encode("m.id.thirdparty", forKey: .type)
+ try container.encode(medium, forKey: .medium)
+ try container.encode(address, forKey: .address)
+ case .phone(country: let country, phone: let phone):
+ try container.encode("m.id.phone", forKey: .type)
+ try container.encode(country, forKey: .country)
+ try container.encode(phone, forKey: .phone)
+ }
+ }
+}
+
+public struct MatrixLoginRequest: Codable {
+ /// The login type being used. One of: ["m.login.password", "m.login.token"]
+ public var type: String
+
+ /// Identification information for the user.
+ public var identifier: MatrixLoginUserIdentifier?
+
+ /// The fully qualified user ID or just local part of the user ID, to log in.
+ @available(*, deprecated, message: "Deprecated in favour of identifier.", renamed: "identifier")
+ public var user: String?
+
+ /// When logging in using a third party identifier, the medium of the identifier. Must be 'email'.
+ @available(*, deprecated, message: "Deprecated in favour of identifier.", renamed: "identifier")
+ public var medium: String?
+
+ /// Third party identifier for the user
+ @available(*, deprecated, message: "Deprecated in favour of identifier.", renamed: "identifier")
+ public var address: String?
+
+ /// Required when type is m.login.password. The user's password.
+ public var password: String?
+
+ /// Required when type is m.login.token. Part of Token-based login.
+ public var token: String?
+
+ /// ID of the client device. If this does not correspond to a known client device, a new device will be created.
+ /// The server will auto-generate a device_id if this is not specified.
+ public var deviceId: String?
+
+ /// A display name to assign to the newly-created device. Ignored if device_id corresponds to a known device.
+ public var initialDeviceDisplayName: String?
+
+ enum CodingKeys: String, CodingKey {
+ case type
+ case identifier
+ case user
+ case medium
+ case address
+ case password
+ case token
+ case deviceId = "device_id"
+ case initialDeviceDisplayName = "initial_device_display_name"
+ }
+}
+
+public struct MatrixLoginResponse: Codable {
+ /// The fully-qualified Matrix ID that has been registered.
+ public var userId: String?
+
+ /// An access token for the account. This access token can then be used to authorize other requests.
+ public var accessToken: String?
+
+ /// The server_name of the homeserver on which the account has been registered.
+ @available(*, deprecated, message: "Clients should extract the server_name from userId (by splitting at the first colon) if they require it.")
+ public var homeServer: String?
+
+ /// ID of the logged-in device. Will be the same as the corresponding parameter in the request, if one was specified.
+ public var deviceId: String?
+
+ /// Optional client configuration provided by the server. If present, clients SHOULD use the provided object to reconfigure
+ /// themselves, optionally validating the URLs within. This object takes the same form as the one returned from .well-known autodiscovery.
+ public var wellKnown: MatrixWellKnown?
+
+ enum CodingKeys: String, CodingKey {
+ case userId = "user_id"
+ case accessToken = "access_token"
+ case homeServer = "home_server"
+ case deviceId = "device_id"
+ case wellKnown = "well_known"
+ }
+}
diff --git a/Sources/MatrixClient/MatrixClient.swift b/Sources/MatrixClient/MatrixClient.swift
new file mode 100644
index 0000000..9e0a493
--- /dev/null
+++ b/Sources/MatrixClient/MatrixClient.swift
@@ -0,0 +1,120 @@
+//
+// MatrixClient.swift
+// File
+//
+// Created by Finn Behrens on 27.09.21.
+//
+
+import Foundation
+
+public struct MatrixClient {
+ public var homeserver: MatrixHomeserver
+ public var urlSession: URLSession = URLSession(configuration: URLSessionConfiguration.default)
+ /// Access token used to authorize api requests
+ public var accessToken: String?
+
+ // 2.1 GET /_maftrix/client/versions
+ /// Gets the versions of the specification supported by the server.
+ ///
+ /// Values will take the form `rX.Y.Z`.
+ ///
+ /// Only the latest `Z` value will be reported for each supported `X.Y` value. i.e. if the server implements `r0.0.0`, `r0.0.1`, and
+ /// `r1.2.0`, it will report `r0.0.1` and `r1.2.0`.
+ ///
+ /// The server may additionally advertise experimental features it supports through unstable_features. These features should be namespaced
+ /// and may optionally include version information within their name if desired. Features listed here are not for optionally toggling parts of the
+ /// Matrix specification and should only be used to advertise support for a feature which has not yet landed in the spec. For example, a feature
+ /// currently undergoing the proposal process may appear here and eventually be taken off this list once the feature lands in the spec and the
+ /// server deems it reasonable to do so. Servers may wish to keep advertising features here after they've been released into the spec to give
+ /// clients a chance to upgrade appropriately. Additionally, clients should avoid using unstable features in their stable releases.
+ ///
+ /// ```markdown
+ /// Rate-limited: No.
+ /// Requires auth: No.
+ /// ```
+ public func getVersions() async throws -> MatrixServerInfo {
+ return try await request("/_matrix/client/versions", withAuthorization: false, forType: MatrixServerInfo.self)
+ }
+
+ /// Gets discovery information about the domain. The file may include additional keys, which MUST follow the Java package naming convention,
+ /// e.g. `com.example.myapp.property`. This ensures property names are suitably namespaced for each application and reduces the risk of clashes.
+ ///
+ /// Note that this endpoint is not necessarily handled by the homeserver, but by another webserver, to be used for discovering the homeserver URL.
+ ///
+ ///```markdown
+ /// Rate-limited: No.
+ /// Requires auth: No.
+ ///```
+ public func getWellKnown() async throws -> MatrixWellKnown {
+ return try await request("/.well-known/matrix/client", withAuthorization: false, forType: MatrixWellKnown.self)
+ }
+
+ /// Gets the homeserver's supported login types to authenticate users. Clients should pick one of these and supply it as the type when logging in.
+ ///
+ /// ```markdown
+ /// Rate-limited: Yes.
+ /// Requires auth: No.
+ /// ```
+ public func getLoginFlows() async throws -> [MatrixLoginFlow] {
+ struct FlowResponse: Codable {
+ var flows: [FlowType]
+
+ struct FlowType: Codable {
+ var type: MatrixLoginFlow
+ }
+ }
+
+ let flows = try await request("/_matrix/client/r0/login", withAuthorization: false, forType: FlowResponse.self)
+
+ return flows.flows.map({$0.type})
+ }
+
+ // Mark: - Helpers
+ func urlComponents(_ path: String) throws -> URL {
+ var components = self.homeserver.url
+
+ components.path = path
+
+ guard let url = components.url else {
+ throw MatrixError.NotFound
+ }
+ return url
+ }
+
+ func urlRequest(_ path: String, withAuthorization: Bool = true) throws -> URLRequest {
+ var requst = URLRequest(url: try urlComponents(path))
+
+ if withAuthorization {
+ guard let accessToken = accessToken else {
+ throw MatrixError.Forbidden
+ }
+ requst.addValue("Bearer \(accessToken)", forHTTPHeaderField: "Authorization")
+ }
+
+ return requst
+ }
+
+ func request(_ path: String, withAuthorization: Bool = true) async throws -> Data {
+ let request = try urlRequest(path, withAuthorization: withAuthorization)
+
+ let (data, urlResponse) = try await urlSession.data(for: request)
+
+ guard let response = urlResponse as? HTTPURLResponse else {
+ throw MatrixError.Unknown
+ }
+ guard response.statusCode == 200 else {
+ throw try MatrixServerError(json: data, code: response.statusCode)
+ }
+
+ return data
+ }
+
+ func request<T: Decodable>(_ path: String, withAuthorization: Bool = true, forType type: T.Type) async throws -> T {
+ let data = try await request(path, withAuthorization: withAuthorization)
+
+ let decoder = JSONDecoder()
+ return try decoder.decode(type, from: data)
+ }
+}
+
+
diff --git a/Tests/MatrixClientTests/ClientTests.swift b/Tests/MatrixClientTests/ClientTests.swift
new file mode 100644
index 0000000..181a0c3
--- /dev/null
+++ b/Tests/MatrixClientTests/ClientTests.swift
@@ -0,0 +1,63 @@
+//
+// File.swift
+// File
+//
+// Created by Finn Behrens on 27.09.21.
+//
+
+import XCTest
+@testable import MatrixClient
+
+final class MatrixClientTests: XCTestCase {
+ let client = MatrixClient(homeserver: MatrixHomeserver(string: "https://matrix-client.matrix.org")!)
+
+ func testGetVersions() async throws {
+ let version = try await client.getVersions()
+
+ // Unlikely that the matrix.org homeserver will ever have less than 2 supported versions
+ XCTAssertGreaterThanOrEqual(version.versions.count, 2)
+ }
+
+ func testGetWellKnown() async throws {
+ let client = MatrixClient(homeserver: MatrixHomeserver(string: "https://matrix.org")!)
+ let wellKnown = try await client.getWellKnown()
+
+ XCTAssertEqual(wellKnown.homeServerBaseUrl, "https://matrix-client.matrix.org")
+ XCTAssertEqual(wellKnown.identityServerBaseUrl, "https://vector.im")
+ }
+
+ func testDecodeWellKnownExtraOptions() throws {
+ let str = "{\"m.homeserver\": { \"base_url\": \"test\" }, \"dev.kloenk.client.test\": 1337 }"
+ let data = Data(str.utf8)
+
+ let decoder = JSONDecoder()
+ let wellKnown = try decoder.decode(MatrixWellKnown.self, from: data)
+
+ XCTAssertEqual(wellKnown.homeServerBaseUrl, "test")
+ XCTAssertEqual(wellKnown.extraInfo["dev.kloenk.client.test"]?.value as? Int, 1337)
+ }
+
+ func testEncodeAndDecodeWellKnownExtraOptions() throws {
+ let orig = MatrixWellKnown(homeserver: MatrixWellKnown.ServerInformation(baseURL: "test"), identityServer: nil, extraInfo: [
+ "dev.kloenk.client.test": 1337,
+ "dev.kloenk.client.test.str": "foobar"
+ ])
+
+ let encoder = JSONEncoder()
+ let encoded = try encoder.encode(orig)
+
+ let decoder = JSONDecoder()
+ let decoded = try decoder.decode(MatrixWellKnown.self, from: encoded)
+
+ XCTAssertEqual(orig.extraInfo["dev.kloenk.client.test"]!.value as! Int, decoded.extraInfo["dev.kloenk.client.test"]!.value as! Int)
+ XCTAssertEqual(orig.extraInfo["dev.kloenk.client.test.str"]!.value as! String, decoded.extraInfo["dev.kloenk.client.test.str"]!.value as! String)
+ XCTAssertEqual(orig.homeServerBaseUrl, "test")
+ }
+
+ func testGetLoginFlows() async throws {
+ let flows = try await client.getLoginFlows()
+
+ // Unlikely that the matrix.org homeserver will ever have less than 2 supported login flows
+ XCTAssertGreaterThanOrEqual(flows.count, 2)
+ }
+}