Skip to content

Commit 7eb7a9f

Browse files
committed
chore(nat): simplify mocking
1 parent 8aeadf2 commit 7eb7a9f

2 files changed

Lines changed: 55 additions & 110 deletions

File tree

libp2p/services/nat/plum_mapper.nim

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,8 +29,10 @@ logScope:
2929
topics = "libp2p natservice plum"
3030

3131
const
32-
DefaultDiscoverTimeout* = 10.seconds
33-
DefaultMappingTimeout* = 10.seconds
32+
# Module-private fallbacks for PlumMapper.new()'s own default args; the public
33+
# NAT timeout knobs live in natservice (DefaultDiscoveryTimeout/MappingTimeout).
34+
DefaultDiscoverTimeout = 10.seconds
35+
DefaultMappingTimeout = 10.seconds
3436

3537
type
3638
MappingKey = tuple[externalPort: uint16, proto: MapProto]

tests/libp2p/services/test_natservice.nim

Lines changed: 51 additions & 108 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,9 @@ type
2626
MockPortMapper = ref object of PortMapper
2727
extIp: IpAddress
2828
extPortQueue: seq[Port]
29+
## external ports handed out in order across successive map() calls; once
30+
## exhausted, map() echoes the requested port. Models an IGD assigning (or
31+
## reassigning) a different external port than requested.
2932
extPortIdx: int
3033
mapErr: Opt[string]
3134
calls: seq[MockCall]
@@ -37,6 +40,10 @@ proc newMock(
3740
): MockPortMapper =
3841
MockPortMapper(extIp: extIp, mapErr: mapErr, extPortQueue: extPorts)
3942

43+
proc mapperFactory(m: MockPortMapper): PortMapperFactory =
44+
proc(mode: PortMappingMode): Opt[PortMapper] {.gcsafe, raises: [].} =
45+
Opt.some(PortMapper(m))
46+
4047
method map*(
4148
self: MockPortMapper, internalPort: Port, externalPort: Port, proto: MapProto
4249
): Future[Result[MappedPort, string]] {.async: (raises: [CancelledError]), gcsafe.} =
@@ -65,15 +72,23 @@ method unmap*(
6572
method close*(self: MockPortMapper) {.async: (raises: []), gcsafe.} =
6673
self.calls.add(MockCall(kind: mckClose))
6774

75+
proc callsOfKind(m: MockPortMapper, kind: MockCallKind): seq[MockCall] =
76+
m.calls.filterIt(it.kind == kind)
77+
6878
proc countCalls(m: MockPortMapper, kind: MockCallKind): int =
69-
for c in m.calls:
70-
if c.kind == kind:
71-
result.inc
79+
m.callsOfKind(kind).len
80+
81+
proc mapCalls(m: MockPortMapper): seq[MockCall] =
82+
m.callsOfKind(mckMap)
83+
84+
proc unmapCalls(m: MockPortMapper): seq[MockCall] =
85+
m.callsOfKind(mckUnmap)
7286

7387
proc unmappedPorts(m: MockPortMapper): seq[Port] =
74-
for c in m.calls:
75-
if c.kind == mckUnmap:
76-
result.add(c.externalPort)
88+
m.unmapCalls.mapIt(it.externalPort)
89+
90+
proc closed(m: MockPortMapper): bool =
91+
m.countCalls(mckClose) > 0
7792

7893
proc standardBuilder(listenAddrs: seq[MultiAddress]): SwitchBuilder =
7994
SwitchBuilder
@@ -162,10 +177,7 @@ suite "NATService":
162177

163178
asyncTest "Upnp maps private listen addrs to extIp/extPort":
164179
let mock = newMock()
165-
let factory: PortMapperFactory = proc(
166-
mode: PortMappingMode
167-
): Opt[PortMapper] {.gcsafe, raises: [].} =
168-
Opt.some(PortMapper(mock))
180+
let factory = mapperFactory(mock)
169181

170182
let switch = makeSwitch(upnpConfig(), @[TcpAutoAddress], factory)
171183
await switch.start()
@@ -184,10 +196,7 @@ suite "NATService":
184196

185197
asyncTest "Upnp preserves already-public listenAddrs alongside mapped ones":
186198
let mock = newMock(extPorts = @[Port(9000)])
187-
let factory: PortMapperFactory = proc(
188-
mode: PortMappingMode
189-
): Opt[PortMapper] {.gcsafe, raises: [].} =
190-
Opt.some(PortMapper(mock))
199+
let factory = mapperFactory(mock)
191200

192201
let switch = makeSwitch(upnpConfig(), @[TcpAutoAddress], factory)
193202
await switch.start()
@@ -204,10 +213,7 @@ suite "NATService":
204213

205214
asyncTest "Upnp unmaps stale extPort when IGD reassigns on refresh":
206215
let mock = newMock(extPorts = @[Port(9000), Port(9001)])
207-
let factory: PortMapperFactory = proc(
208-
mode: PortMappingMode
209-
): Opt[PortMapper] {.gcsafe, raises: [].} =
210-
Opt.some(PortMapper(mock))
216+
let factory = mapperFactory(mock)
211217

212218
let switch = makeSwitch(upnpConfig(), @[TcpAutoAddress], factory)
213219
await switch.start()
@@ -226,10 +232,7 @@ suite "NATService":
226232

227233
asyncTest "Upnp unmaps everything when private listenAddrs disappear":
228234
let mock = newMock(extPorts = @[Port(7000)])
229-
let factory: PortMapperFactory = proc(
230-
mode: PortMappingMode
231-
): Opt[PortMapper] {.gcsafe, raises: [].} =
232-
Opt.some(PortMapper(mock))
235+
let factory = mapperFactory(mock)
233236

234237
let switch = makeSwitch(upnpConfig(), @[TcpAutoAddress], factory)
235238
await switch.start()
@@ -258,10 +261,7 @@ suite "NATService":
258261

259262
asyncTest "stop unmaps active mappings and closes the mapper":
260263
let mock = newMock(extPorts = @[Port(5555)])
261-
let factory: PortMapperFactory = proc(
262-
mode: PortMappingMode
263-
): Opt[PortMapper] {.gcsafe, raises: [].} =
264-
Opt.some(PortMapper(mock))
264+
let factory = mapperFactory(mock)
265265

266266
let switch = makeSwitch(upnpConfig(), @[TcpAutoAddress], factory)
267267
await switch.start()
@@ -299,10 +299,7 @@ suite "NATService":
299299

300300
asyncTest "map failure leaves no stale entry; announced falls through":
301301
let mock = newMock(extPorts = @[Port(8000)], mapErr = Opt.some("mapping refused"))
302-
let factory: PortMapperFactory = proc(
303-
mode: PortMappingMode
304-
): Opt[PortMapper] {.gcsafe, raises: [].} =
305-
Opt.some(PortMapper(mock))
302+
let factory = mapperFactory(mock)
306303

307304
let switch = makeSwitch(upnpConfig(), @[TcpAutoAddress], factory)
308305
await switch.start()
@@ -320,10 +317,7 @@ suite "NATService":
320317
# libplum maps IPv4 only, so any IPv6 listenAddr (even ULA fc00::/7) must be
321318
# filtered out before map().
322319
let mock = newMock()
323-
let factory: PortMapperFactory = proc(
324-
mode: PortMappingMode
325-
): Opt[PortMapper] {.gcsafe, raises: [].} =
326-
Opt.some(PortMapper(mock))
320+
let factory = mapperFactory(mock)
327321

328322
let switch = makeSwitch(upnpConfig(), @[TcpAutoAddress], factory)
329323
await switch.start()
@@ -385,10 +379,7 @@ suite "NATService":
385379
# NATConfig keeps mode (port-mapping) and autonat orthogonal: enabling
386380
# both must spin up the UPnP addressMapper *and* the AutonatService.
387381
let mock = newMock()
388-
let factory: PortMapperFactory = proc(
389-
mode: PortMappingMode
390-
): Opt[PortMapper] {.gcsafe, raises: [].} =
391-
Opt.some(PortMapper(mock))
382+
let factory = mapperFactory(mock)
392383

393384
var cfg = upnpConfig()
394385
cfg.reachability = autonatConfig(AutonatV1).reachability
@@ -457,10 +448,7 @@ suite "NATService":
457448

458449
asyncTest "withNAT can be called once per distinct concern":
459450
let mock = newMock()
460-
let factory: PortMapperFactory = proc(
461-
mode: PortMappingMode
462-
): Opt[PortMapper] {.gcsafe, raises: [].} =
463-
Opt.some(PortMapper(mock))
451+
let factory = mapperFactory(mock)
464452

465453
let switch = standardBuilder(@[TcpAutoAddress])
466454
.withNAT(upnpConfig(), factory)
@@ -482,50 +470,6 @@ suite "NATService":
482470
.withNAT(autonatConfig(AutonatV1))
483471
.withNAT(autonatConfig(AutonatV2))
484472

485-
type RecordingPortMapper = ref object of PortMapper
486-
externalIp: IpAddress
487-
mapErr: Opt[string]
488-
mapPortOverride: Opt[Port]
489-
## When set, `map` returns this port instead of echoing the requested
490-
## `externalPort`. Models an IGD that re-maps to a different external port
491-
## (e.g. when the requested one is busy).
492-
unmapResult: Result[void, string]
493-
mapCalls: seq[tuple[internal, external: Port, proto: MapProto]]
494-
unmapCalls: seq[tuple[external: Port, proto: MapProto]]
495-
closed: bool
496-
497-
method map*(
498-
self: RecordingPortMapper, internalPort: Port, externalPort: Port, proto: MapProto
499-
): Future[Result[MappedPort, string]] {.async: (raises: [CancelledError]), gcsafe.} =
500-
self.mapCalls.add((internalPort, externalPort, proto))
501-
self.mapErr.withValue(e):
502-
return err(e)
503-
let ext = self.mapPortOverride.get(externalPort)
504-
ok(MappedPort(externalIp: self.externalIp, externalPort: ext))
505-
506-
method unmap*(
507-
self: RecordingPortMapper, externalPort: Port, proto: MapProto
508-
): Future[Result[void, string]] {.async: (raises: [CancelledError]), gcsafe.} =
509-
self.unmapCalls.add((externalPort, proto))
510-
self.unmapResult
511-
512-
method close*(self: RecordingPortMapper) {.async: (raises: []), gcsafe.} =
513-
self.closed = true
514-
515-
proc newRecordingOk(externalIp: IpAddress): RecordingPortMapper =
516-
RecordingPortMapper(externalIp: externalIp, unmapResult: Result[void, string].ok())
517-
518-
proc recordingFactory(m: RecordingPortMapper): PortMapperFactory =
519-
return proc(mode: PortMappingMode): Opt[PortMapper] {.gcsafe, raises: [].} =
520-
Opt.some(PortMapper(m))
521-
522-
proc recordingFactoryFail(): PortMapperFactory =
523-
recordingFactory(
524-
RecordingPortMapper(
525-
mapErr: Opt.some("mock no IGD"), unmapResult: Result[void, string].ok()
526-
)
527-
)
528-
529473
proc loopbackAddr(): MultiAddress =
530474
MultiAddress.init("/ip4/127.0.0.1/tcp/0").get()
531475

@@ -539,8 +483,8 @@ suite "NATService (setupMappings)":
539483
asyncTest "NatPmp announces external IP after successful mapping":
540484
let
541485
externalIp = parseIpAddress("203.0.113.99")
542-
mapper = newRecordingOk(externalIp)
543-
factory = recordingFactory(mapper)
486+
mapper = newMock(extIp = externalIp)
487+
factory = mapperFactory(mapper)
544488
cfg = natPmpConfig()
545489
switch = makeSwitch(cfg, @[loopbackAddr()], factory)
546490
svc = findNatService(switch)
@@ -559,8 +503,8 @@ suite "NATService (setupMappings)":
559503
# on a v4 listen address, so the mapping produces no announced address
560504
let
561505
externalIp = parseIpAddress("2001:db8::1")
562-
mapper = newRecordingOk(externalIp)
563-
factory = recordingFactory(mapper)
506+
mapper = newMock(extIp = externalIp)
507+
factory = mapperFactory(mapper)
564508
cfg = upnpConfig()
565509
switch = makeSwitch(cfg, @[loopbackAddr()], factory)
566510
svc = findNatService(switch)
@@ -576,8 +520,8 @@ suite "NATService (setupMappings)":
576520
asyncTest "non-private listen addresses are skipped":
577521
let
578522
externalIp = parseIpAddress("203.0.113.1")
579-
mapper = newRecordingOk(externalIp)
580-
factory = recordingFactory(mapper)
523+
mapper = newMock(extIp = externalIp)
524+
factory = mapperFactory(mapper)
581525
cfg = upnpConfig()
582526
switch = makeSwitch(cfg, @[loopbackAddr()], factory)
583527
svc = findNatService(switch)
@@ -601,8 +545,8 @@ suite "NATService (setupMappings)":
601545
let
602546
externalIp = parseIpAddress("203.0.113.111")
603547
userAddr = MultiAddress.init("/ip4/198.51.100.7/tcp/4242").tryGet()
604-
mapper = newRecordingOk(externalIp)
605-
factory = recordingFactory(mapper)
548+
mapper = newMock(extIp = externalIp)
549+
factory = mapperFactory(mapper)
606550
cfg = upnpConfig()
607551
switch = makeSwitch(cfg, @[loopbackAddr()], factory)
608552

@@ -620,8 +564,8 @@ suite "NATService (setupMappings)":
620564
asyncTest "multiple private listen addresses are each mapped once":
621565
let
622566
externalIp = parseIpAddress("203.0.113.10")
623-
mapper = newRecordingOk(externalIp)
624-
factory = recordingFactory(mapper)
567+
mapper = newMock(extIp = externalIp)
568+
factory = mapperFactory(mapper)
625569
cfg = upnpConfig()
626570
switch = makeSwitch(cfg, @[loopbackAddr()], factory)
627571
svc = findNatService(switch)
@@ -647,8 +591,8 @@ suite "NATService (setupMappings)":
647591
asyncTest "setupMappings unmaps stale ports when a listen addr is removed":
648592
let
649593
externalIp = parseIpAddress("203.0.113.20")
650-
mapper = newRecordingOk(externalIp)
651-
factory = recordingFactory(mapper)
594+
mapper = newMock(extIp = externalIp)
595+
factory = mapperFactory(mapper)
652596
cfg = upnpConfig()
653597
switch = makeSwitch(cfg, @[loopbackAddr()], factory)
654598
svc = findNatService(switch)
@@ -665,22 +609,20 @@ suite "NATService (setupMappings)":
665609
# Second cycle: one of them is gone. unmapStale must clean it up.
666610
discard await svc.setupMappings(@[privateAddr(9000)])
667611
check mapper.unmapCalls.len == 1
668-
check mapper.unmapCalls[^1].external == Port(9001)
612+
check mapper.unmapCalls[^1].externalPort == Port(9001)
669613
check mapper.unmapCalls[^1].proto == mpTcp
670614

671615
asyncTest "IGD returning a different external port surfaces in announced":
672616
let
673617
externalIp = parseIpAddress("203.0.113.30")
674-
mapper = newRecordingOk(externalIp)
675-
factory = recordingFactory(mapper)
618+
# queued external port simulates the IGD remapping the request to a
619+
# different port (e.g. because the requested one is already busy).
620+
mapper = newMock(extIp = externalIp, extPorts = @[Port(54321)])
621+
factory = mapperFactory(mapper)
676622
cfg = upnpConfig()
677623
switch = makeSwitch(cfg, @[loopbackAddr()], factory)
678624
svc = findNatService(switch)
679625

680-
# Simulate the IGD remapping the request to a different external port
681-
# (e.g. because the requested one is already busy on the gateway).
682-
mapper.mapPortOverride = Opt.some(Port(54321))
683-
684626
await switch.start()
685627
defer:
686628
await switch.stop()
@@ -694,7 +636,8 @@ suite "NATService (setupMappings)":
694636
asyncTest "NatPmp mapping failure leaves announced empty":
695637
let
696638
cfg = natPmpConfig()
697-
switch = makeSwitch(cfg, @[loopbackAddr()], recordingFactoryFail())
639+
mapper = newMock(mapErr = Opt.some("mock no IGD"))
640+
switch = makeSwitch(cfg, @[loopbackAddr()], mapperFactory(mapper))
698641
svc = findNatService(switch)
699642

700643
await switch.start()
@@ -708,8 +651,8 @@ suite "NATService (setupMappings)":
708651
asyncTest "NatPmp stop unmaps all created mappings":
709652
let
710653
externalIp = parseIpAddress("203.0.113.40")
711-
mapper = newRecordingOk(externalIp)
712-
factory = recordingFactory(mapper)
654+
mapper = newMock(extIp = externalIp)
655+
factory = mapperFactory(mapper)
713656
cfg = natPmpConfig()
714657
switch = makeSwitch(cfg, @[loopbackAddr()], factory)
715658
svc = findNatService(switch)

0 commit comments

Comments
 (0)