github.com/openfga/openfga@v1.5.4-rc1/tests/listusers/listusers_test.go (about)

     1  package listusers
     2  
     3  import (
     4  	"bytes"
     5  	"context"
     6  	"encoding/json"
     7  	"fmt"
     8  	"io"
     9  	"net/http"
    10  	"testing"
    11  
    12  	openfgav1 "github.com/openfga/api/proto/openfga/v1"
    13  	parser "github.com/openfga/language/pkg/go/transformer"
    14  	"github.com/stretchr/testify/require"
    15  	"go.uber.org/goleak"
    16  	"go.uber.org/zap"
    17  	"go.uber.org/zap/zaptest/observer"
    18  	"google.golang.org/grpc"
    19  
    20  	"github.com/openfga/openfga/cmd/run"
    21  	"github.com/openfga/openfga/internal/mocks"
    22  	"github.com/openfga/openfga/internal/server/config"
    23  	"github.com/openfga/openfga/pkg/logger"
    24  	"github.com/openfga/openfga/pkg/testutils"
    25  	"github.com/openfga/openfga/pkg/typesystem"
    26  	"github.com/openfga/openfga/tests"
    27  )
    28  
    29  func TestListUsersMemory(t *testing.T) {
    30  	testRunAll(t, "memory")
    31  }
    32  
    33  func TestListUsersPostgres(t *testing.T) {
    34  	testRunAll(t, "postgres")
    35  }
    36  
    37  func TestListUsersMySQL(t *testing.T) {
    38  	testRunAll(t, "mysql")
    39  }
    40  
    41  func testRunAll(t *testing.T, engine string) {
    42  	t.Cleanup(func() {
    43  		goleak.VerifyNone(t)
    44  	})
    45  	cfg := config.MustDefaultConfig()
    46  	cfg.Log.Level = "error"
    47  	cfg.Datastore.Engine = engine
    48  	cfg.Experimentals = []string{"enable-list-users"}
    49  
    50  	tests.StartServer(t, cfg)
    51  
    52  	conn := testutils.CreateGrpcConnection(t, cfg.GRPC.Addr)
    53  
    54  	RunAllTests(t, openfgav1.NewOpenFGAServiceClient(conn))
    55  }
    56  
    57  func TestListUsersLogs(t *testing.T) {
    58  	// uncomment after https://github.com/openfga/openfga/pull/1199 is done. the span exporter needs to be closed properly
    59  	// defer goleak.VerifyNone(t)
    60  
    61  	// create mock OTLP server
    62  	otlpServerPort, otlpServerPortReleaser := testutils.TCPRandomPort()
    63  	localOTLPServerURL := fmt.Sprintf("localhost:%d", otlpServerPort)
    64  	otlpServerPortReleaser()
    65  	_ = mocks.NewMockTracingServer(t, otlpServerPort)
    66  
    67  	cfg := config.MustDefaultConfig()
    68  	cfg.Trace.Enabled = true
    69  	cfg.Trace.OTLP.Endpoint = localOTLPServerURL
    70  	cfg.Datastore.Engine = "memory"
    71  	cfg.Experimentals = append(cfg.Experimentals, "enable-list-users")
    72  
    73  	observerLogger, logs := observer.New(zap.DebugLevel)
    74  	serverCtx := &run.ServerContext{
    75  		Logger: &logger.ZapLogger{
    76  			Logger: zap.New(observerLogger),
    77  		},
    78  	}
    79  
    80  	// We're starting a full fledged server because the logs we
    81  	// want to observe are emitted on the interceptors/middleware layer.
    82  	tests.StartServerWithContext(t, cfg, serverCtx)
    83  
    84  	conn := testutils.CreateGrpcConnection(t, cfg.GRPC.Addr,
    85  		grpc.WithUserAgent("test-user-agent"),
    86  	)
    87  	client := openfgav1.NewOpenFGAServiceClient(conn)
    88  
    89  	createStoreResp, err := client.CreateStore(context.Background(), &openfgav1.CreateStoreRequest{
    90  		Name: "openfga-demo",
    91  	})
    92  	require.NoError(t, err)
    93  
    94  	storeID := createStoreResp.GetId()
    95  
    96  	model := parser.MustTransformDSLToProto(`model
    97  	schema 1.1
    98  type user
    99  
   100  type document
   101    relations
   102  	define viewer: [user]`)
   103  
   104  	writeModelResp, err := client.WriteAuthorizationModel(context.Background(), &openfgav1.WriteAuthorizationModelRequest{
   105  		StoreId:         storeID,
   106  		SchemaVersion:   typesystem.SchemaVersion1_1,
   107  		TypeDefinitions: model.GetTypeDefinitions(),
   108  		Conditions:      model.GetConditions(),
   109  	})
   110  	require.NoError(t, err)
   111  
   112  	authorizationModelID := writeModelResp.GetAuthorizationModelId()
   113  
   114  	_, err = client.Write(context.Background(), &openfgav1.WriteRequest{
   115  		StoreId: storeID,
   116  		Writes: &openfgav1.WriteRequestWrites{
   117  			TupleKeys: []*openfgav1.TupleKey{
   118  				{Object: "document:1", Relation: "viewer", User: "user:anne"},
   119  			},
   120  		},
   121  	})
   122  	require.NoError(t, err)
   123  
   124  	logs.TakeAll()
   125  
   126  	type test struct {
   127  		_name           string
   128  		grpcReq         *openfgav1.ListUsersRequest
   129  		httpReqBody     io.Reader
   130  		expectedError   bool
   131  		expectedContext map[string]interface{}
   132  	}
   133  
   134  	tests := []test{
   135  		{
   136  			_name: "grpc_list_users_success",
   137  			grpcReq: &openfgav1.ListUsersRequest{
   138  				StoreId:              storeID,
   139  				AuthorizationModelId: authorizationModelID,
   140  				Relation:             "viewer",
   141  				Object: &openfgav1.Object{
   142  					Type: "document",
   143  					Id:   "1",
   144  				},
   145  				UserFilters: []*openfgav1.UserTypeFilter{{Type: "user"}},
   146  			},
   147  			expectedContext: map[string]interface{}{
   148  				"grpc_service":           "openfga.v1.OpenFGAService",
   149  				"grpc_method":            "ListUsers",
   150  				"grpc_type":              "unary",
   151  				"grpc_code":              int32(0),
   152  				"raw_request":            fmt.Sprintf(`{"store_id":"%s","relation":"viewer","object":{"type":"document","id":"1"},"user_filters":[{"type":"user","relation":""}], "contextual_tuples":[],"authorization_model_id":"%s","context":null}`, storeID, authorizationModelID),
   153  				"raw_response":           `{"excluded_users":[],"users":[{"object":{"type":"user","id":"anne"}}]}`,
   154  				"authorization_model_id": authorizationModelID,
   155  				"store_id":               storeID,
   156  				"user_agent":             "test-user-agent" + " grpc-go/" + grpc.Version,
   157  			},
   158  		},
   159  		{
   160  			_name: "http_list_users_success",
   161  			httpReqBody: bytes.NewBufferString(`{
   162  		  "authorization_model_id": "` + authorizationModelID + `",
   163  		  "relation":"viewer",
   164  		  "object":{"type":"document","id":"1"},
   165  		  "user_filters":[{"type":"user"}]
   166  		}`),
   167  			expectedContext: map[string]interface{}{
   168  				"grpc_service":           "openfga.v1.OpenFGAService",
   169  				"grpc_method":            "ListUsers",
   170  				"grpc_type":              "unary",
   171  				"grpc_code":              int32(0),
   172  				"raw_request":            fmt.Sprintf(`{"store_id":"%s","relation":"viewer","object":{"type":"document","id":"1"},"user_filters":[{"type":"user","relation":""}], "contextual_tuples":[],"authorization_model_id":"%s","context":null}`, storeID, authorizationModelID),
   173  				"raw_response":           `{"excluded_users":[],"users":[{"object":{"type":"user","id":"anne"}}]}`,
   174  				"authorization_model_id": authorizationModelID,
   175  				"store_id":               storeID,
   176  				"user_agent":             "test-user-agent",
   177  			},
   178  		},
   179  		{
   180  			_name: "grpc_list_users_error",
   181  			grpcReq: &openfgav1.ListUsersRequest{
   182  				StoreId:              storeID,
   183  				AuthorizationModelId: authorizationModelID,
   184  				Relation:             "viewer",
   185  				// Object field is missing
   186  				UserFilters: []*openfgav1.UserTypeFilter{{Type: "user"}},
   187  			},
   188  			expectedError: true,
   189  			expectedContext: map[string]interface{}{
   190  				"grpc_service": "openfga.v1.OpenFGAService",
   191  				"grpc_method":  "ListUsers",
   192  				"grpc_type":    "unary",
   193  				"grpc_code":    int32(2000),
   194  				"raw_request":  fmt.Sprintf(`{"store_id":"%s","relation":"viewer","object":null,"user_filters":[{"type":"user","relation":""}], "contextual_tuples":[],"authorization_model_id":"%s","context":null}`, storeID, authorizationModelID),
   195  				"raw_response": `{"code":"validation_error", "message":"invalid ListUsersRequest.Object: value is required"}`,
   196  				"store_id":     storeID,
   197  				"user_agent":   "test-user-agent" + " grpc-go/" + grpc.Version,
   198  			},
   199  		},
   200  		{
   201  			_name: "http_list_users_error",
   202  			httpReqBody: bytes.NewBufferString(`{
   203  				"authorization_model_id": "` + authorizationModelID + `",
   204  				"relation":"viewer",
   205  				"user_filters":[{"type":"user"}]
   206  			  }`),
   207  			expectedError: true,
   208  			expectedContext: map[string]interface{}{
   209  				"grpc_service": "openfga.v1.OpenFGAService",
   210  				"grpc_method":  "ListUsers",
   211  				"grpc_type":    "unary",
   212  				"grpc_code":    int32(2000),
   213  				"raw_request":  fmt.Sprintf(`{"store_id":"%s","relation":"viewer","object":null,"user_filters":[{"type":"user","relation":""}], "contextual_tuples":[],"authorization_model_id":"%s","context":null}`, storeID, authorizationModelID),
   214  				"raw_response": `{"code":"validation_error", "message":"invalid ListUsersRequest.Object: value is required"}`,
   215  				"store_id":     storeID,
   216  				"user_agent":   "test-user-agent",
   217  			},
   218  		},
   219  	}
   220  
   221  	for _, test := range tests {
   222  		t.Run(test._name, func(t *testing.T) {
   223  			// clear observed logs after each run. We expect each test to log one line
   224  			defer logs.TakeAll()
   225  
   226  			if test.grpcReq != nil {
   227  				_, err = client.ListUsers(context.Background(), test.grpcReq)
   228  			} else if test.httpReqBody != nil {
   229  				var httpReq *http.Request
   230  				httpReq, err = http.NewRequest("POST", "http://"+cfg.HTTP.Addr+"/stores/"+storeID+"/list-users", test.httpReqBody)
   231  				require.NoError(t, err)
   232  
   233  				httpReq.Header.Set("User-Agent", "test-user-agent")
   234  				client := &http.Client{}
   235  
   236  				_, err = client.Do(httpReq)
   237  			}
   238  			if test.expectedError && test.grpcReq != nil {
   239  				require.Error(t, err)
   240  			} else {
   241  				require.NoError(t, err)
   242  			}
   243  
   244  			actualLogs := logs.All()
   245  			require.Len(t, actualLogs, 1)
   246  
   247  			fields := actualLogs[0].ContextMap()
   248  			require.Equal(t, test.expectedContext["grpc_service"], fields["grpc_service"])
   249  			require.Equal(t, test.expectedContext["grpc_method"], fields["grpc_method"])
   250  			require.Equal(t, test.expectedContext["grpc_type"], fields["grpc_type"])
   251  			require.Equal(t, test.expectedContext["grpc_code"], fields["grpc_code"])
   252  			require.JSONEq(t, test.expectedContext["raw_request"].(string), string(fields["raw_request"].(json.RawMessage)))
   253  			require.JSONEq(t, test.expectedContext["raw_response"].(string), string(fields["raw_response"].(json.RawMessage)))
   254  			require.Equal(t, test.expectedContext["authorization_model_id"], fields["authorization_model_id"])
   255  			require.Equal(t, test.expectedContext["store_id"], fields["store_id"])
   256  			require.Equal(t, test.expectedContext["user_agent"], fields["user_agent"])
   257  			require.NotEmpty(t, fields["peer.address"])
   258  			require.NotEmpty(t, fields["request_id"])
   259  			require.NotEmpty(t, fields["trace_id"])
   260  			if !test.expectedError {
   261  				require.NotEmpty(t, fields["datastore_query_count"])
   262  				require.Len(t, fields, 13)
   263  			} else {
   264  				require.Len(t, fields, 12)
   265  			}
   266  		})
   267  	}
   268  }