api-swift / Tests / IrdAPITests / Live / LiveConformanceTests.swift
1,206 words
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
import Foundation
import Testing
@testable import IrdAPI

/// The coverage table pins the client to the spec; this sweep pins the spec to the live
/// API. Every read endpoint is fetched raw and checked against its declared schema
/// (undocumented fields included), and decoded through the typed method. Endpoints
/// whose resource the account does not have are skipped; three account-only endpoints
/// are expected to answer 401 to an API key. Not covered because the resource only
/// exists mid-flight or costs money: /jobs/{id}, /jobs/{id}/result, /ipfs/uploads/{id},
/// /pins/{requestid}, /vps/actions/{id}.
@Suite(.enabled(if: LiveEnv.apiKey != nil), .serialized) struct LiveConformanceTests {
    struct Endpoint {
        let path: String
        let params: [String: String]
        let query: [QueryItem]
        let expected: Int
        let typed: (@Sendable (IrdClient) async throws -> Void)?
        let anonymous: Bool

        init(_ path: String, params: [String: String] = [:], query: [QueryItem] = [], expected: Int = 200, anonymous: Bool = false,
             typed: (@Sendable (IrdClient) async throws -> Void)?) {
            self.path = path; self.params = params; self.query = query; self.expected = expected; self.anonymous = anonymous; self.typed = typed
        }
    }

    static func endpoints(_ c: IrdClient) async -> [Endpoint] {
        func first<T>(_ f: () async throws -> T?) async -> T? { (try? await f()) ?? nil }
        let regId = await first { try await c.registrations.list().registrations.first?.id }
        let gwId = await first { try await c.ipfs.listDomains().domains.first?.id }
        let nameId = await first { try await c.ipfs.listNames().names.first?.id }
        let pinId = await first { try await c.ipfs.listPins().pins.first?.id }
        let appId = await first { try await c.p2p.listApps().apps.first?.id }
        let tunnelId = await first { try await c.tunnels.list().tunnels.first?.id }
        let instanceId = await first { try await c.vps.listInstances().instances.first?.id }
        let deploymentId = await first { try await c.vps.listAppDeployments().deployments.first?.id }
        let actionId = await first { try await c.registrations.listActions().actions.first?.id }
        let threadId = await first { try await c.support.listThreads().threads.first?.id }
        let claimId = await first { try await c.dns.listClaims().claims.first?.id }
        let zone = await first { try await c.dns.listZones().domains.first?.origin }.map { $0.hasSuffix(".") ? String($0.dropLast()) : $0 }

        var list: [Endpoint] = [
            Endpoint("/credits/balance") { _ = try await $0.credits.balance() },
            Endpoint("/credits/ledger") { _ = try await $0.credits.ledger() },
            Endpoint("/credits/prices") { _ = try await $0.credits.prices() },
            Endpoint("/contact/threads") { _ = try await $0.support.listThreads() },
            Endpoint("/domains/check", query: [QueryItem("domain", "ird.ro")], anonymous: true) { _ = try await $0.domains.check(domain: "ird.ro") },
            Endpoint("/domains/info", query: [QueryItem("domain", "ird.ro")]) { _ = try await $0.domains.info(domain: "ird.ro") },
            Endpoint("/domains/preflight", query: [QueryItem("domain", "ird.ro")]) { _ = try await $0.domains.preflight(domain: "ird.ro") },
            Endpoint("/domains/dns-snapshot", query: [QueryItem("domain", "ird.ro")]) { _ = try await $0.domains.dnsSnapshot(domain: "ird.ro") },
            Endpoint("/domains/search", query: [QueryItem("q", "shop")]) { _ = try await $0.domains.search(q: "shop") },
            Endpoint("/domains/example") { _ = try await $0.domains.example() },
            Endpoint("/domains/directory", query: [QueryItem("limit", "20")]) { _ = try await $0.domains.directory(query: DirectoryQuery(limit: 20)) },
            Endpoint("/domains/directory/{domain}", params: ["domain": "ird.ro"]) { _ = try await $0.domains.directoryEntry(domain: "ird.ro") },
            Endpoint("/domains/registrations", query: [QueryItem("limit", "100")]) { _ = try await $0.registrations.list(query: RegistrationListQuery(limit: 100)) },
            Endpoint("/domains/actions", query: [QueryItem("limit", "100")]) { _ = try await $0.registrations.listActions(query: ListActionsQuery(limit: 100)) },
            Endpoint("/dns/domains") { _ = try await $0.dns.listZones() },
            Endpoint("/dns/claims") { _ = try await $0.dns.listClaims() },
            Endpoint("/ipfs/pins", query: [QueryItem("limit", "100")]) { _ = try await $0.ipfs.listPins(query: ListPinsQuery(limit: 100)) },
            Endpoint("/ipfs/names") { _ = try await $0.ipfs.listNames() },
            Endpoint("/ipfs/domains") { _ = try await $0.ipfs.listDomains() },
            Endpoint("/ipfs/usage") { _ = try await $0.ipfs.usage() },
            Endpoint("/pins", query: [QueryItem("limit", "100")]) { _ = try await $0.pins.list(query: PinningServiceListQuery(limit: 100)) },
            Endpoint("/p2p/apps") { _ = try await $0.p2p.listApps() },
            Endpoint("/p2p/usage") { _ = try await $0.p2p.usage() },
            Endpoint("/tunnels") { _ = try await $0.tunnels.list() },
            Endpoint("/vps/tiers") { _ = try await $0.vps.listTiers() },
            Endpoint("/vps/system-images") { _ = try await $0.vps.listSystemImages() },
            Endpoint("/vps/ssh-keys") { _ = try await $0.vps.listSshKeys() },
            Endpoint("/vps/availability") { _ = try await $0.vps.availability() },
            Endpoint("/vps/instances") { _ = try await $0.vps.listInstances() },
            Endpoint("/vps/apps") { _ = try await $0.vps.appCatalog() },
            Endpoint("/vps/apps/deployments") { _ = try await $0.vps.listAppDeployments() },
            Endpoint("/vps/volumes") { _ = try await $0.vps.listVolumes() },
            Endpoint("/vps/images") { _ = try await $0.vps.listImages() },
            Endpoint("/vps/activity") { _ = try await $0.vps.listActivity() },
            Endpoint("/grants", expected: 401) { _ = try await $0.sharing.listGrants() },
            Endpoint("/vps/transfers", expected: 401) { _ = try await $0.transfers.list() },
        ]
        if let threadId { list.append(Endpoint("/contact/threads/{id}", params: ["id": threadId]) { _ = try await $0.support.getThread(id: threadId) }) }
        if let id = regId {
            list += [
                Endpoint("/domains/registrations/{id}", params: ["id": id]) { _ = try await $0.registrations.get(id: id) },
                Endpoint("/domains/registrations/{id}/hosts", params: ["id": id]) { _ = try await $0.registrations.listHosts(id: id) },
                Endpoint("/domains/registrations/{id}/dsdata", params: ["id": id]) { _ = try await $0.registrations.listDsData(id: id) },
                Endpoint("/domains/registrations/{id}/trade", params: ["id": id]) { _ = try await $0.registrations.quoteTrade(id: id) },
                Endpoint("/domains/registrations/{id}/dns/dnssec", params: ["id": id]) { _ = try await $0.dns.getDnssec(id: id) },
                Endpoint("/domains/registrations/{id}/dns/nameservers", params: ["id": id]) { _ = try await $0.dns.hostedNameservers(id: id) },
                Endpoint("/domains/registrations/{id}/grants", params: ["id": id], expected: 401) { _ = try await $0.sharing.listDomainGrants(id: id) },
            ]
        }
        if let actionId { list.append(Endpoint("/domains/actions/{id}", params: ["id": actionId]) { _ = try await $0.registrations.getAction(id: actionId) }) }
        if let zone { list.append(Endpoint("/dns/domains/{domain}", params: ["domain": zone]) { _ = try await $0.dns.getZone(domain: zone) }) }
        if let claimId { list.append(Endpoint("/dns/claims/{id}", params: ["id": claimId]) { _ = try await $0.dns.getClaim(id: claimId) }) }
        if let pinId { list.append(Endpoint("/ipfs/pins/{id}", params: ["id": pinId]) { _ = try await $0.ipfs.getPin(id: pinId) }) }
        if let nameId { list.append(Endpoint("/ipfs/names/{id}", params: ["id": nameId]) { _ = try await $0.ipfs.getName(id: nameId) }) }
        if let gwId {
            list.append(Endpoint("/ipfs/domains/{id}", params: ["id": gwId]) { _ = try await $0.ipfs.getDomain(id: gwId) })
            list.append(Endpoint("/ipfs/domains/{id}/pins", params: ["id": gwId]) { _ = try await $0.ipfs.listDomainPins(id: gwId) })
        }
        if let appId {
            list.append(Endpoint("/p2p/apps/{id}", params: ["id": appId]) { _ = try await $0.p2p.getApp(id: appId) })
            list.append(Endpoint("/p2p/apps/{id}/domains", params: ["id": appId]) { _ = try await $0.p2p.listAppDomains(id: appId) })
        }
        if let tunnelId { list.append(Endpoint("/tunnels/{id}", params: ["id": tunnelId]) { _ = try await $0.tunnels.get(id: tunnelId) }) }
        if let instanceId {
            list += [
                Endpoint("/vps/instances/{id}", params: ["id": instanceId]) { _ = try await $0.vps.getInstance(id: instanceId) },
                Endpoint("/vps/instances/{id}/metrics", params: ["id": instanceId]) { _ = try await $0.vps.getMetrics(id: instanceId) },
                Endpoint("/vps/instances/{id}/domains", params: ["id": instanceId]) { _ = try await $0.vps.listInstanceDomains(id: instanceId) },
            ]
        }
        if let deploymentId { list.append(Endpoint("/vps/apps/deployments/{id}", params: ["id": deploymentId]) { _ = try await $0.vps.getAppDeployment(id: deploymentId) }) }
        return list
    }

    @Test func liveResponsesMatchTheSpecAndDecode() async throws {
        let client = LiveEnv.client
        var drifted: [String] = []
        var exercised = 0
        for e in await Self.endpoints(client) {
            exercised += 1
            let caller = e.anonymous ? IrdClient() : client
            var status = 200
            var body: JSONValue? = nil
            do {
                body = try await caller.raw.request(method: .get, path: e.path, params: e.params, query: e.query,
                                                    security: e.anonymous ? .anonymous : .keyOrJwt)
            } catch let err as IrdError {
                guard case .api(let s, _, _, let b) = err else { throw err }
                status = s
                body = b
            }
            var problems: [String] = []
            if status != e.expected {
                problems.append("expected HTTP \(e.expected) but got \(status)")
            } else if let body {
                problems = SpecShape.check(SpecShape.responseSchema(e.path, status: status), body)
                if status == 200, let typed = e.typed {
                    do { try await typed(client) } catch { problems.append("typed decode failed: \(error)") }
                }
            }
            if !problems.isEmpty {
                drifted.append(e.path)
                print("DRIFT \(e.path)")
                for p in problems { print("  - \(p)") }
            }
        }
        print("conformance: \(exercised) endpoints, \(exercised - drifted.count) conform, \(drifted.count) drifted")
        #expect(exercised > 0)
        #expect(drifted.isEmpty, "the spec and the live API disagree; see the DRIFT report above")
    }
}

static mirror of HEAD · about · clone: git clone https://ird.cod-sursa.ro/api-swift.git