mirror of
https://github.com/tailscale/tailscale.git
synced 2025-08-22 02:50:42 +00:00
ipn/ipn{server,test}: extract the LocalAPI test client and server into ipntest
In this PR, we extract the in-process LocalAPI client/server implementation from ipn/ipnserver/server_test.go into a new ipntest package to be used in high‑level black‑box tests, such as those for the tailscale CLI. Updates #15575 Signed-off-by: Nick Khyl <nickk@tailscale.com>
This commit is contained in:
170
ipn/lapitest/opts.go
Normal file
170
ipn/lapitest/opts.go
Normal file
@@ -0,0 +1,170 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
package lapitest
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"tailscale.com/control/controlclient"
|
||||
"tailscale.com/ipn/ipnlocal"
|
||||
"tailscale.com/tsd"
|
||||
"tailscale.com/tstest"
|
||||
"tailscale.com/types/lazy"
|
||||
"tailscale.com/types/logger"
|
||||
)
|
||||
|
||||
// Option is any optional configuration that can be passed to [NewServer] or [NewBackend].
|
||||
type Option interface {
|
||||
apply(*options) error
|
||||
}
|
||||
|
||||
// options is the merged result of all applied [Option]s.
|
||||
type options struct {
|
||||
tb testing.TB
|
||||
ctx lazy.SyncValue[context.Context]
|
||||
logf lazy.SyncValue[logger.Logf]
|
||||
sys lazy.SyncValue[*tsd.System]
|
||||
newCC lazy.SyncValue[NewControlFn]
|
||||
backend lazy.SyncValue[*ipnlocal.LocalBackend]
|
||||
}
|
||||
|
||||
// newOptions returns a new [options] struct with the specified [Option]s applied.
|
||||
func newOptions(tb testing.TB, opts ...Option) (*options, error) {
|
||||
options := &options{tb: tb}
|
||||
for _, opt := range opts {
|
||||
if err := opt.apply(options); err != nil {
|
||||
return nil, fmt.Errorf("lapitest: %w", err)
|
||||
}
|
||||
}
|
||||
return options, nil
|
||||
}
|
||||
|
||||
// TB returns the owning [*testing.T] or [*testing.B].
|
||||
func (o *options) TB() testing.TB {
|
||||
return o.tb
|
||||
}
|
||||
|
||||
// Context returns the base context to be used by the server.
|
||||
func (o *options) Context() context.Context {
|
||||
return o.ctx.Get(context.Background)
|
||||
}
|
||||
|
||||
// Logf returns the [logger.Logf] to be used for logging.
|
||||
func (o *options) Logf() logger.Logf {
|
||||
return o.logf.Get(func() logger.Logf { return logger.Discard })
|
||||
}
|
||||
|
||||
// Sys returns the [tsd.System] that contains subsystems to be used
|
||||
// when creating a new [ipnlocal.LocalBackend].
|
||||
func (o *options) Sys() *tsd.System {
|
||||
return o.sys.Get(func() *tsd.System { return tsd.NewSystem() })
|
||||
}
|
||||
|
||||
// Backend returns the [ipnlocal.LocalBackend] to be used by the server.
|
||||
// If a backend is provided via [WithBackend], it is used as-is.
|
||||
// Otherwise, a new backend is created with the the [options] in o.
|
||||
func (o *options) Backend() *ipnlocal.LocalBackend {
|
||||
return o.backend.Get(func() *ipnlocal.LocalBackend { return newBackend(o) })
|
||||
}
|
||||
|
||||
// MakeControlClient returns a new [controlclient.Client] to be used by newly
|
||||
// created [ipnlocal.LocalBackend]s. It is only used if no backend is provided
|
||||
// via [WithBackend].
|
||||
func (o *options) MakeControlClient(opts controlclient.Options) (controlclient.Client, error) {
|
||||
newCC := o.newCC.Get(func() NewControlFn { return NewUnreachableControlClient })
|
||||
return newCC(o.tb, opts)
|
||||
}
|
||||
|
||||
type loggingOption struct{ enableLogging bool }
|
||||
|
||||
// WithLogging returns an [Option] that enables or disables logging.
|
||||
func WithLogging(enableLogging bool) Option {
|
||||
return loggingOption{enableLogging: enableLogging}
|
||||
}
|
||||
|
||||
func (o loggingOption) apply(opts *options) error {
|
||||
var logf logger.Logf
|
||||
if o.enableLogging {
|
||||
logf = tstest.WhileTestRunningLogger(opts.tb)
|
||||
} else {
|
||||
logf = logger.Discard
|
||||
}
|
||||
if !opts.logf.Set(logf) {
|
||||
return errors.New("logging already configured")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type contextOption struct{ ctx context.Context }
|
||||
|
||||
// WithContext returns an [Option] that sets the base context to be used by the [Server].
|
||||
func WithContext(ctx context.Context) Option {
|
||||
return contextOption{ctx: ctx}
|
||||
}
|
||||
|
||||
func (o contextOption) apply(opts *options) error {
|
||||
if !opts.ctx.Set(o.ctx) {
|
||||
return errors.New("context already configured")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type sysOption struct{ sys *tsd.System }
|
||||
|
||||
// WithSys returns an [Option] that sets the [tsd.System] to be used
|
||||
// when creating a new [ipnlocal.LocalBackend].
|
||||
func WithSys(sys *tsd.System) Option {
|
||||
return sysOption{sys: sys}
|
||||
}
|
||||
|
||||
func (o sysOption) apply(opts *options) error {
|
||||
if !opts.sys.Set(o.sys) {
|
||||
return errors.New("tsd.System already configured")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type backendOption struct{ backend *ipnlocal.LocalBackend }
|
||||
|
||||
// WithBackend returns an [Option] that configures the server to use the specified
|
||||
// [ipnlocal.LocalBackend] instead of creating a new one.
|
||||
// It is mutually exclusive with [WithControlClient].
|
||||
func WithBackend(backend *ipnlocal.LocalBackend) Option {
|
||||
return backendOption{backend: backend}
|
||||
}
|
||||
|
||||
func (o backendOption) apply(opts *options) error {
|
||||
if _, ok := opts.backend.Peek(); ok {
|
||||
return errors.New("backend cannot be set when control client is already set")
|
||||
}
|
||||
if !opts.backend.Set(o.backend) {
|
||||
return errors.New("backend already set")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// NewControlFn is any function that creates a new [controlclient.Client]
|
||||
// with the specified options.
|
||||
type NewControlFn func(tb testing.TB, opts controlclient.Options) (controlclient.Client, error)
|
||||
|
||||
// WithControlClient returns an option that specifies a function to be used
|
||||
// by the [ipnlocal.LocalBackend] when creating a new [controlclient.Client].
|
||||
// It is mutually exclusive with [WithBackend] and is only used if no backend
|
||||
// has been provided.
|
||||
func WithControlClient(newControl NewControlFn) Option {
|
||||
return newControl
|
||||
}
|
||||
|
||||
func (fn NewControlFn) apply(opts *options) error {
|
||||
if _, ok := opts.backend.Peek(); ok {
|
||||
return errors.New("control client cannot be set when backend is already set")
|
||||
}
|
||||
if !opts.newCC.Set(fn) {
|
||||
return errors.New("control client already set")
|
||||
}
|
||||
return nil
|
||||
}
|
Reference in New Issue
Block a user