github.com/openfga/openfga@v1.5.4-rc1/pkg/testutils/testutils.go (about)

     1  // Package testutils contains code that is useful in tests.
     2  package testutils
     3  
     4  import (
     5  	"context"
     6  	"errors"
     7  	"fmt"
     8  	"math/rand"
     9  	"net"
    10  	"net/http"
    11  	"sort"
    12  	"strconv"
    13  	"testing"
    14  	"time"
    15  
    16  	"github.com/cenkalti/backoff/v4"
    17  	"github.com/google/go-cmp/cmp"
    18  	"github.com/hashicorp/go-retryablehttp"
    19  	"github.com/oklog/ulid/v2"
    20  	openfgav1 "github.com/openfga/api/proto/openfga/v1"
    21  	parser "github.com/openfga/language/pkg/go/transformer"
    22  	"github.com/stretchr/testify/require"
    23  	"google.golang.org/grpc"
    24  
    25  	grpcbackoff "google.golang.org/grpc/backoff"
    26  	"google.golang.org/grpc/credentials"
    27  	"google.golang.org/grpc/credentials/insecure"
    28  	healthv1pb "google.golang.org/grpc/health/grpc_health_v1"
    29  	"google.golang.org/protobuf/types/known/structpb"
    30  
    31  	serverconfig "github.com/openfga/openfga/internal/server/config"
    32  )
    33  
    34  const (
    35  	AllChars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
    36  )
    37  
    38  var (
    39  	TupleCmpTransformer = cmp.Transformer("Sort", func(in []*openfgav1.Tuple) []*openfgav1.Tuple {
    40  		out := append([]*openfgav1.Tuple(nil), in...) // Copy input to avoid mutating it
    41  
    42  		sort.SliceStable(out, func(i, j int) bool {
    43  			if out[i].GetKey().GetObject() != out[j].GetKey().GetObject() {
    44  				return out[i].GetKey().GetObject() < out[j].GetKey().GetObject()
    45  			}
    46  
    47  			if out[i].GetKey().GetRelation() != out[j].GetKey().GetRelation() {
    48  				return out[i].GetKey().GetRelation() < out[j].GetKey().GetRelation()
    49  			}
    50  
    51  			if out[i].GetKey().GetUser() != out[j].GetKey().GetUser() {
    52  				return out[i].GetKey().GetUser() < out[j].GetKey().GetUser()
    53  			}
    54  
    55  			return true
    56  		})
    57  
    58  		return out
    59  	})
    60  	TupleKeyCmpTransformer = cmp.Transformer("Sort", func(in []*openfgav1.TupleKey) []*openfgav1.TupleKey {
    61  		out := append([]*openfgav1.TupleKey(nil), in...) // Copy input to avoid mutating it
    62  
    63  		sort.SliceStable(out, func(i, j int) bool {
    64  			if out[i].GetObject() != out[j].GetObject() {
    65  				return out[i].GetObject() < out[j].GetObject()
    66  			}
    67  
    68  			if out[i].GetRelation() != out[j].GetRelation() {
    69  				return out[i].GetRelation() < out[j].GetRelation()
    70  			}
    71  
    72  			if out[i].GetUser() != out[j].GetUser() {
    73  				return out[i].GetUser() < out[j].GetUser()
    74  			}
    75  
    76  			return true
    77  		})
    78  
    79  		return out
    80  	})
    81  )
    82  
    83  func CreateRandomString(n int) string {
    84  	b := make([]byte, n)
    85  	for i := range b {
    86  		b[i] = AllChars[rand.Intn(len(AllChars))]
    87  	}
    88  	return string(b)
    89  }
    90  
    91  func MustNewStruct(t require.TestingT, v map[string]interface{}) *structpb.Struct {
    92  	conditionContext, err := structpb.NewStruct(v)
    93  	require.NoError(t, err)
    94  	return conditionContext
    95  }
    96  
    97  // MakeSliceWithGenerator generates a slice of length 'n' and populates the contents
    98  // with values based on the generator provided.
    99  func MakeSliceWithGenerator[T any](n uint64, generator func(n uint64) any) []T {
   100  	s := make([]T, 0, n)
   101  
   102  	for i := uint64(0); i < n; i++ {
   103  		s = append(s, generator(i).(T))
   104  	}
   105  
   106  	return s
   107  }
   108  
   109  // NumericalStringGenerator generates a string representation of the provided
   110  // uint value.
   111  func NumericalStringGenerator(n uint64) any {
   112  	return strconv.FormatUint(n, 10)
   113  }
   114  
   115  func MakeStringWithRuneset(n uint64, runeSet []rune) string {
   116  	var s string
   117  	for i := uint64(0); i < n; i++ {
   118  		s += string(runeSet[rand.Intn(len(runeSet))])
   119  	}
   120  
   121  	return s
   122  }
   123  
   124  // MustTransformDSLToProtoWithID interprets the provided string s as an FGA model and
   125  // attempts to parse it using the official OpenFGA language parser. The model returned
   126  // includes an auto-generated model id which assists with producing models for testing
   127  // purposes.
   128  func MustTransformDSLToProtoWithID(s string) *openfgav1.AuthorizationModel {
   129  	model := parser.MustTransformDSLToProto(s)
   130  	model.Id = ulid.Make().String()
   131  
   132  	return model
   133  }
   134  
   135  // CreateGrpcConnection creates a grpc connection to an address and closes it when the test ends.
   136  func CreateGrpcConnection(t *testing.T, grpcAddress string, opts ...grpc.DialOption) *grpc.ClientConn {
   137  	t.Helper()
   138  
   139  	defaultOptions := []grpc.DialOption{
   140  		grpc.WithConnectParams(grpc.ConnectParams{Backoff: grpcbackoff.DefaultConfig}),
   141  		grpc.WithTransportCredentials(insecure.NewCredentials()),
   142  	}
   143  
   144  	defaultOptions = append(defaultOptions, opts...)
   145  
   146  	conn, err := grpc.Dial(
   147  		grpcAddress, defaultOptions...,
   148  	)
   149  	require.NoError(t, err)
   150  	t.Cleanup(func() {
   151  		conn.Close()
   152  	})
   153  
   154  	return conn
   155  }
   156  
   157  // EnsureServiceHealthy is a test helper that ensures that a service's grpc health endpoint is responding OK. It can also
   158  // ensure that the HTTP /healthz endpoint is responding OK. If the service doesn't respond healthy in 30 seconds it fails the test.
   159  func EnsureServiceHealthy(t testing.TB, grpcAddr, httpAddr string, transportCredentials credentials.TransportCredentials, httpHealthCheck bool) {
   160  	t.Helper()
   161  
   162  	creds := insecure.NewCredentials()
   163  	if transportCredentials != nil {
   164  		creds = transportCredentials
   165  	}
   166  
   167  	dialOpts := []grpc.DialOption{
   168  		grpc.WithTransportCredentials(creds),
   169  		grpc.WithConnectParams(grpc.ConnectParams{Backoff: grpcbackoff.DefaultConfig}),
   170  	}
   171  
   172  	ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
   173  	defer cancel()
   174  
   175  	t.Log("creating connection to address", grpcAddr)
   176  	conn, err := grpc.DialContext(
   177  		ctx,
   178  		grpcAddr,
   179  		dialOpts...,
   180  	)
   181  	require.NoError(t, err, "error creating grpc connection to server")
   182  	t.Cleanup(func() {
   183  		conn.Close()
   184  	})
   185  
   186  	client := healthv1pb.NewHealthClient(conn)
   187  
   188  	policy := backoff.NewExponentialBackOff()
   189  	policy.MaxElapsedTime = 30 * time.Second
   190  
   191  	err = backoff.Retry(func() error {
   192  		resp, err := client.Check(context.Background(), &healthv1pb.HealthCheckRequest{
   193  			Service: openfgav1.OpenFGAService_ServiceDesc.ServiceName,
   194  		})
   195  		if err != nil {
   196  			t.Log(time.Now(), "not serving yet at address", grpcAddr, err)
   197  			return err
   198  		}
   199  
   200  		if resp.GetStatus() != healthv1pb.HealthCheckResponse_SERVING {
   201  			t.Log(time.Now(), resp.GetStatus())
   202  			return errors.New("not serving")
   203  		}
   204  
   205  		return nil
   206  	}, policy)
   207  	require.NoError(t, err, "server did not reach healthy status")
   208  
   209  	if httpHealthCheck {
   210  		resp, err := retryablehttp.Get(fmt.Sprintf("http://%s/healthz", httpAddr))
   211  		require.NoError(t, err, "http endpoint not healthy")
   212  
   213  		t.Cleanup(func() {
   214  			err := resp.Body.Close()
   215  			require.NoError(t, err)
   216  		})
   217  
   218  		require.Equal(t, http.StatusOK, resp.StatusCode, "unexpected status code received from server")
   219  	}
   220  }
   221  
   222  // MustDefaultConfigWithRandomPorts returns default server config but with random ports for the grpc and http addresses
   223  // and with the playground, tracing and metrics turned off.
   224  // This function may panic if somehow a random port cannot be chosen.
   225  func MustDefaultConfigWithRandomPorts() *serverconfig.Config {
   226  	config := serverconfig.MustDefaultConfig()
   227  
   228  	httpPort, httpPortReleaser := TCPRandomPort()
   229  	defer httpPortReleaser()
   230  	grpcPort, grpcPortReleaser := TCPRandomPort()
   231  	defer grpcPortReleaser()
   232  
   233  	config.GRPC.Addr = fmt.Sprintf("0.0.0.0:%d", grpcPort)
   234  	config.HTTP.Addr = fmt.Sprintf("0.0.0.0:%d", httpPort)
   235  
   236  	return config
   237  }
   238  
   239  // TCPRandomPort tries to find a random TCP Port. If it can't find one, it panics. Else, it returns the port and a function that releases the port.
   240  // It is the responsibility of the caller to call the release function right before trying to listen on the given port.
   241  func TCPRandomPort() (int, func()) {
   242  	l, err := net.Listen("tcp", "")
   243  	if err != nil {
   244  		panic(err)
   245  	}
   246  	return l.Addr().(*net.TCPAddr).Port, func() {
   247  		l.Close()
   248  	}
   249  }