| 1 | import { assert, describe, it } from "@effect/vitest" |
| 2 | import { assertTrue } from "@effect/vitest/utils" |
| 3 | import { Equal, Hash, Result, Schema } from "effect" |
| 4 | import * as IpInterface from "effect/net/IpInterface" |
| 5 | import * as IpNetwork from "effect/net/IpNetwork" |
| 6 | import * as NetAddress from "effect/net/NetAddress" |
| 7 | import { inspect } from "node:util" |
| 8 | |
| 9 | const success = <A>(result: Result.Result<A, unknown>): A => { |
| 10 | assertTrue(Result.isSuccess(result), "expected Success") |
| 11 | return result.success |
| 12 | } |
| 13 | |
| 14 | const failure = <E>(result: Result.Result<unknown, E>): E => { |
| 15 | assertTrue(Result.isFailure(result), "expected Failure") |
| 16 | return result.failure |
| 17 | } |
| 18 | |
| 19 | const ip = (input: string): NetAddress.IpAddress => success(NetAddress.ipFromString(input)) |
| 20 | const interfaceAddress = (input: string): IpInterface.IpInterface => success(IpInterface.fromString(input)) |
| 21 | |
| 22 | describe("IpInterface", () => { |
| 23 | it("serializes and inspects canonical strings", () => { |
| 24 | for (const expected of ["192.0.2.1/24", "2001:db8::1/64"]) { |
| 25 | const address = IpInterface.fromStringUnsafe(expected) |
| 26 | assert.strictEqual(address.toJSON(), expected) |
| 27 | assert.strictEqual(JSON.stringify(address), JSON.stringify(expected)) |
| 28 | assert.strictEqual(inspect(address), expected) |
| 29 | } |
| 30 | }) |
| 31 | |
| 32 | it("retains the address supplied to checked constructors", () => { |
| 33 | const address = NetAddress.ipv4Loopback |
| 34 | assert.strictEqual(failure(IpInterface.make(address, 33)).input, address) |
| 35 | }) |
| 36 | |
| 37 | it("forwards errors from address and prefix validation", () => { |
| 38 | for (const parse of [IpInterface.fromString, IpInterface.ipv4FromString]) { |
| 39 | assert.strictEqual(failure(parse("1.2.3.4/+24")).input, "1.2.3.4/+24") |
| 40 | assert.strictEqual(failure(parse("010.2.3.4/24")).input, "010.2.3.4") |
| 41 | assert.deepStrictEqual(failure(parse("1.2.3.4/33")).input, ip("1.2.3.4")) |
| 42 | } |
| 43 | for (const parse of [IpInterface.fromString, IpInterface.ipv6FromString]) { |
| 44 | assert.strictEqual(failure(parse("::ffff:192.000.2.1/128")).input, "192.000.2.1") |
| 45 | } |
| 46 | }) |
| 47 | |
| 48 | it("constructs one generic runtime representation", () => { |
| 49 | const ipv4 = IpInterface.makeUnsafe(NetAddress.ipv4Unspecified, 0) |
| 50 | const ipv6 = IpInterface.makeUnsafe(NetAddress.ipv6Unspecified, 0) |
| 51 | assert.isTrue(Object.isFrozen(ipv4)) |
| 52 | assert.isTrue(Object.isFrozen(ipv6)) |
| 53 | assert.strictEqual(ipv4._tag, "IpInterface") |
| 54 | assert.strictEqual(ipv6._tag, "IpInterface") |
| 55 | assert.isTrue(IpInterface.isIpv4Interface(ipv4)) |
| 56 | assert.isTrue(IpInterface.isIpv6Interface(ipv6)) |
| 57 | assert.isTrue(IpInterface.isIpInterface(ipv4)) |
| 58 | }) |
| 59 | |
| 60 | it("defaults bare addresses to full-width prefixes", () => { |
| 61 | assert.strictEqual(IpInterface.format(interfaceAddress("192.0.2.1")), "192.0.2.1/32") |
| 62 | assert.strictEqual(IpInterface.format(interfaceAddress("2001:db8::1")), "2001:db8::1/128") |
| 63 | }) |
| 64 | |
| 65 | it("parses prefixes and preserves host bits", () => { |
| 66 | assert.strictEqual(IpInterface.format(interfaceAddress("192.0.2.1/24")), "192.0.2.1/24") |
| 67 | assert.strictEqual( |
| 68 | IpInterface.format(interfaceAddress("2001:0DB8:0000:0000:0000:0000:0000:0001/64")), |
| 69 | "2001:db8::1/64" |
| 70 | ) |
| 71 | }) |
| 72 | |
| 73 | it("rejects invalid prefixes and malformed input", () => { |
| 74 | const address = ip("10.1.2.3") |
| 75 | for (const prefix of [-1, 1.5, Number.NaN, Number.POSITIVE_INFINITY, 33]) { |
| 76 | failure(IpInterface.make(address, prefix)) |
| 77 | } |
| 78 | assert.strictEqual(failure(IpInterface.make(address, -1))._tag, "NetAddressError") |
| 79 | assert.strictEqual( |
| 80 | failure(IpInterface.fromString("1.2.3.4/+24")).message, |
| 81 | "prefix length must be an unpadded ASCII decimal integer" |
| 82 | ) |
| 83 | failure(IpInterface.fromString("1.2.3.4", { prefix: "required" })) |
| 84 | failure(IpInterface.ipv4FromString("::1/128")) |
| 85 | failure(IpInterface.ipv6FromString("127.0.0.1/32")) |
| 86 | }) |
| 87 | |
| 88 | it("uses address and prefix equality with matching hashes", () => { |
| 89 | const first = interfaceAddress("2001:0DB8:0:0:0:0:0:1/64") |
| 90 | const second = interfaceAddress("2001:db8::1/64") |
| 91 | assert.isTrue(Equal.equals(first, second)) |
| 92 | assert.strictEqual(Hash.hash(first), Hash.hash(second)) |
| 93 | assert.isFalse(Equal.equals(first, interfaceAddress("2001:db8::1/65"))) |
| 94 | }) |
| 95 | |
| 96 | it("converts to the canonical containing network", () => { |
| 97 | assert.strictEqual(IpNetwork.format(IpNetwork.fromInterface(interfaceAddress("10.1.2.3/8"))), "10.0.0.0/8") |
| 98 | }) |
| 99 | |
| 100 | it("provides throwing counterparts", () => { |
| 101 | assert.strictEqual(IpInterface.format(IpInterface.fromStringUnsafe("10.1.2.3/8")), "10.1.2.3/8") |
| 102 | assert.throws(() => IpInterface.makeUnsafe(ip("10.1.2.3"), 33)) |
| 103 | }) |
| 104 | |
| 105 | it("decodes and canonically encodes schemas", () => { |
| 106 | const value = Schema.decodeUnknownSync(Schema.IpInterfaceFromString)("192.0.2.1/24") |
| 107 | assert.strictEqual(Schema.encodeSync(Schema.IpInterfaceFromString)(value), "192.0.2.1/24") |
| 108 | }) |
| 109 | }) |