import Foundation
import Testing
@testable import IrdAPI
let registrationsPage = json(200, #"{"registrations":[{"id":"r1","domain":"Prod.RO"},{"id":"r2","domain":"dev.ro"}],"next_cursor":null}"#)
func gatedClient(rules: GateRules, script: [HTTPResponse], apiKey: String? = "ird_k") async throws -> (IrdClient, FakeHTTP) {
let http = FakeHTTP(script: [registrationsPage] + script)
var c = Configuration()
if let apiKey { c.credential = StaticCredential(apiKey) }
c.transport = http
c.clock = InstantClock()
let client = try await IrdClient.gated(c, rules: rules)
return (client, http)
}
@Suite struct DomainGateTests {
@Test func indexIsBuiltEagerlyWithCursorPagination() async throws {
let http = FakeHTTP(script: [
json(200, #"{"registrations":[{"id":"r1","domain":"a.ro"}],"next_cursor":"c2"}"#),
json(200, #"{"registrations":[{"id":"r2","domain":"B.RO."}],"next_cursor":null}"#),
])
var c = Configuration(apiKey: "ird_k")
c.transport = http
let client = try await IrdClient.gated(c, rules: GateRules(deniedDomains: ["b.ro"]))
#expect(http.trail == ["GET /domains/registrations?limit=100", "GET /domains/registrations?limit=100&cursor=c2"])
#expect(await client.gate?.registrationIndex == ["r1": "a.ro", "r2": "b.ro"])
#expect(client.isGated)
}
@Test func registrationIdsResolveThroughTheCacheWithoutARequest() async throws {
let (client, http) = try await gatedClient(rules: GateRules(deniedDomains: ["prod.ro"]), script: [])
let before = http.count
let e = await caught { try await client.checkGate(op: "POST /domains/registrations/{id}/renew", target: .registrationId("r1")) }
#expect(e == .domainDenied(domain: "prod.ro", pattern: "prod.ro", op: "POST /domains/registrations/{id}/renew"))
try await client.checkGate(op: "x", target: .registrationId("r2"))
#expect(http.count == before)
}
@Test func cacheMissFetchesOnceThenCaches() async throws {
let (client, http) = try await gatedClient(rules: GateRules(deniedDomains: ["new.ro"]),
script: [json(200, #"{"registration":{"id":"r9","domain":"New.RO"}}"#)])
let e = await caught { try await client.checkGate(op: "op", target: .registrationId("r9")) }
#expect(e == .domainDenied(domain: "new.ro", pattern: "new.ro", op: "op"))
#expect(http.trail.last == "GET /domains/registrations/r9")
let n = http.count
_ = await caught { try await client.checkGate(op: "op", target: .registrationId("r9")) }
#expect(http.count == n)
}
@Test func unknownIdPassesUnderDenyListAndFailsClosedUnderAllowList() async throws {
let (deny, _) = try await gatedClient(rules: GateRules(deniedDomains: ["prod.ro"]), script: [json(404, #"{"error":{"code":"not_found","message":"no"}}"#)])
try await deny.checkGate(op: "op", target: .registrationId("nope"))
let (allow, _) = try await gatedClient(rules: GateRules(allowedDomains: ["prod.ro"]), script: [json(404, #"{"error":{"code":"not_found","message":"no"}}"#)])
let e = await caught { try await allow.checkGate(op: "op", target: .registrationId("nope")) }
#expect(e == .domainDenied(domain: nil, pattern: nil, op: "op"))
}
@Test func gateRequiresACredential() async {
var c = Configuration()
c.transport = FakeHTTP(script: [registrationsPage])
let e = await caught { _ = try await IrdClient.gated(c, rules: GateRules(deniedDomains: ["x.ro"])) }
#expect(e == .config("Domain gate options require an authenticated client (apiKey)"))
}
@Test func gatedMutationIsRefusedBeforeAnyRequest() async throws {
let (client, http) = try await gatedClient(rules: GateRules(deniedDomains: ["*.ro"]), script: [])
let n = http.count
let e = await caught { _ = try await client.raw.request(method: .post, path: "/tunnels", body: ["hostname": "dev.x.ro"]) }
#expect(e == .domainDenied(domain: "dev.x.ro", pattern: "*.ro", op: "POST /tunnels"))
#expect(http.count == n)
}
@Test func ungatedClientsSkipTheGateEntirely() async throws {
let (client, http) = captureClient()
_ = try await client.raw.request(method: .post, path: "/tunnels", body: ["hostname": "dev.x.ro"])
#expect(http.trail == ["POST /tunnels"])
#expect(!client.isGated)
try await client.refreshGate()
try await client.checkGate(op: "x", target: .domain("anything"))
#expect(http.count == 1)
}
@Test func refreshPicksUpNewRegistrations() async throws {
let (client, http) = try await gatedClient(rules: GateRules(deniedDomains: ["prod.ro"]),
script: [json(200, #"{"registrations":[{"id":"r3","domain":"c.ro"}],"next_cursor":null}"#)])
try await client.refreshGate()
#expect(http.trail.last == "GET /domains/registrations?limit=100")
#expect(await client.gate?.registrationIndex == ["r3": "c.ro"])
}
}