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 }