mirror of
https://github.com/tailscale/tailscale.git
synced 2025-05-29 02:38:32 +00:00

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>
171 lines
5.0 KiB
Go
171 lines
5.0 KiB
Go
// 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
|
|
}
|