1import { assert, describe, it } from "@effect/vitest"
2import { assertTrue } from "@effect/vitest/utils"
3import { Equal, Hash, Result, Schema } from "effect"
4import * as IpInterface from "effect/net/IpInterface"
5import * as IpNetwork from "effect/net/IpNetwork"
6import * as NetAddress from "effect/net/NetAddress"
7import { inspect } from "node:util"
8
9const success = <A>(result: Result.Result<A, unknown>): A => {
10 assertTrue(Result.isSuccess(result), "expected Success")
11 return result.success
12}
13
14const failure = <E>(result: Result.Result<unknown, E>): E => {
15 assertTrue(Result.isFailure(result), "expected Failure")
16 return result.failure
17}
18
19const ip = (input: string): NetAddress.IpAddress => success(NetAddress.ipFromString(input))
20const interfaceAddress = (input: string): IpInterface.IpInterface => success(IpInterface.fromString(input))
21
22describe("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})