github.com/onflow/flow-go@v0.35.7-crescendo-preview.23-atree-inlining/engine/access/rest/routes/transactions_test.go (about) 1 package routes 2 3 import ( 4 "bytes" 5 "encoding/json" 6 "fmt" 7 "net/http" 8 "net/url" 9 "strings" 10 "testing" 11 12 mocks "github.com/stretchr/testify/mock" 13 "golang.org/x/text/cases" 14 "golang.org/x/text/language" 15 "google.golang.org/grpc/codes" 16 "google.golang.org/grpc/status" 17 18 "github.com/onflow/flow/protobuf/go/flow/entities" 19 20 "github.com/onflow/flow-go/access" 21 "github.com/onflow/flow-go/access/mock" 22 "github.com/onflow/flow-go/engine/access/rest/models" 23 "github.com/onflow/flow-go/engine/access/rest/util" 24 "github.com/onflow/flow-go/model/flow" 25 "github.com/onflow/flow-go/utils/unittest" 26 ) 27 28 func getTransactionReq(id string, expandResult bool, blockIdQuery string, collectionIdQuery string) *http.Request { 29 u, _ := url.Parse(fmt.Sprintf("/v1/transactions/%s", id)) 30 q := u.Query() 31 32 if expandResult { 33 // by default expand all since we test expanding with converters 34 q.Add("expand", "result") 35 } 36 37 if blockIdQuery != "" { 38 q.Add("block_id", blockIdQuery) 39 } 40 41 if collectionIdQuery != "" { 42 q.Add("collection_id", collectionIdQuery) 43 } 44 45 u.RawQuery = q.Encode() 46 47 req, _ := http.NewRequest("GET", u.String(), nil) 48 return req 49 } 50 51 func getTransactionResultReq(id string, blockIdQuery string, collectionIdQuery string) *http.Request { 52 u, _ := url.Parse(fmt.Sprintf("/v1/transaction_results/%s", id)) 53 q := u.Query() 54 if blockIdQuery != "" { 55 q.Add("block_id", blockIdQuery) 56 } 57 58 if collectionIdQuery != "" { 59 q.Add("collection_id", collectionIdQuery) 60 } 61 62 u.RawQuery = q.Encode() 63 64 req, _ := http.NewRequest("GET", u.String(), nil) 65 return req 66 } 67 68 func createTransactionReq(body interface{}) *http.Request { 69 jsonBody, _ := json.Marshal(body) 70 req, _ := http.NewRequest("POST", "/v1/transactions", bytes.NewBuffer(jsonBody)) 71 return req 72 } 73 74 func TestGetTransactions(t *testing.T) { 75 t.Run("get by ID without results", func(t *testing.T) { 76 backend := &mock.API{} 77 tx := unittest.TransactionFixture() 78 req := getTransactionReq(tx.ID().String(), false, "", "") 79 80 backend.Mock. 81 On("GetTransaction", mocks.Anything, tx.ID()). 82 Return(&tx.TransactionBody, nil) 83 84 expected := fmt.Sprintf(` 85 { 86 "id":"%s", 87 "script":"YWNjZXNzKGFsbCkgZnVuIG1haW4oKSB7fQ==", 88 "arguments": [], 89 "reference_block_id":"%s", 90 "gas_limit":"10", 91 "payer":"8c5303eaa26202d6", 92 "proposal_key":{ 93 "address":"8c5303eaa26202d6", 94 "key_index":"1", 95 "sequence_number":"0" 96 }, 97 "authorizers":[ 98 "8c5303eaa26202d6" 99 ], 100 "payload_signatures": [], 101 "envelope_signatures":[ 102 { 103 "address":"8c5303eaa26202d6", 104 "key_index":"1", 105 "signature":"%s" 106 } 107 ], 108 "_links":{ 109 "_self":"/v1/transactions/%s" 110 }, 111 "_expandable": { 112 "result": "/v1/transaction_results/%s" 113 } 114 }`, 115 tx.ID(), tx.ReferenceBlockID, util.ToBase64(tx.EnvelopeSignatures[0].Signature), tx.ID(), tx.ID()) 116 117 assertOKResponse(t, req, expected, backend) 118 }) 119 120 t.Run("Get by ID with results", func(t *testing.T) { 121 backend := &mock.API{} 122 123 tx := unittest.TransactionFixture() 124 txr := transactionResultFixture(tx) 125 126 backend.Mock. 127 On("GetTransaction", mocks.Anything, tx.ID()). 128 Return(&tx.TransactionBody, nil) 129 130 backend.Mock. 131 On("GetTransactionResult", mocks.Anything, tx.ID(), flow.ZeroID, flow.ZeroID, entities.EventEncodingVersion_JSON_CDC_V0). 132 Return(txr, nil) 133 134 req := getTransactionReq(tx.ID().String(), true, "", "") 135 136 expected := fmt.Sprintf(` 137 { 138 "id":"%s", 139 "script":"YWNjZXNzKGFsbCkgZnVuIG1haW4oKSB7fQ==", 140 "arguments": [], 141 "reference_block_id":"%s", 142 "gas_limit":"10", 143 "payer":"8c5303eaa26202d6", 144 "proposal_key":{ 145 "address":"8c5303eaa26202d6", 146 "key_index":"1", 147 "sequence_number":"0" 148 }, 149 "authorizers":[ 150 "8c5303eaa26202d6" 151 ], 152 "payload_signatures": [], 153 "envelope_signatures":[ 154 { 155 "address":"8c5303eaa26202d6", 156 "key_index":"1", 157 "signature":"%s" 158 } 159 ], 160 "result": { 161 "block_id": "%s", 162 "collection_id": "%s", 163 "execution": "Success", 164 "status": "Sealed", 165 "status_code": 1, 166 "error_message": "", 167 "computation_used": "0", 168 "events": [ 169 { 170 "type": "flow.AccountCreated", 171 "transaction_id": "%s", 172 "transaction_index": "0", 173 "event_index": "0", 174 "payload": "" 175 } 176 ], 177 "_links": { 178 "_self": "/v1/transaction_results/%s" 179 } 180 }, 181 "_expandable": {}, 182 "_links":{ 183 "_self":"/v1/transactions/%s" 184 } 185 }`, 186 tx.ID(), tx.ReferenceBlockID, util.ToBase64(tx.EnvelopeSignatures[0].Signature), tx.ReferenceBlockID, txr.CollectionID, tx.ID(), tx.ID(), tx.ID()) 187 assertOKResponse(t, req, expected, backend) 188 }) 189 190 t.Run("get by ID Invalid", func(t *testing.T) { 191 backend := &mock.API{} 192 193 req := getTransactionReq("invalid", false, "", "") 194 expected := `{"code":400, "message":"invalid ID format"}` 195 assertResponse(t, req, http.StatusBadRequest, expected, backend) 196 }) 197 198 t.Run("get by ID non-existing", func(t *testing.T) { 199 backend := &mock.API{} 200 201 tx := unittest.TransactionFixture() 202 req := getTransactionReq(tx.ID().String(), false, "", "") 203 204 backend.Mock. 205 On("GetTransaction", mocks.Anything, tx.ID()). 206 Return(nil, status.Error(codes.NotFound, "transaction not found")) 207 208 expected := `{"code":404, "message":"Flow resource not found: transaction not found"}` 209 assertResponse(t, req, http.StatusNotFound, expected, backend) 210 }) 211 } 212 213 func TestGetTransactionResult(t *testing.T) { 214 id := unittest.IdentifierFixture() 215 bid := unittest.IdentifierFixture() 216 cid := unittest.IdentifierFixture() 217 txr := &access.TransactionResult{ 218 Status: flow.TransactionStatusSealed, 219 StatusCode: 10, 220 Events: []flow.Event{ 221 unittest.EventFixture(flow.EventAccountCreated, 1, 0, id, 200), 222 }, 223 ErrorMessage: "", 224 BlockID: bid, 225 CollectionID: cid, 226 } 227 txr.Events[0].Payload = []byte(`test payload`) 228 expected := fmt.Sprintf(`{ 229 "block_id": "%s", 230 "collection_id": "%s", 231 "execution": "Success", 232 "status": "Sealed", 233 "status_code": 10, 234 "error_message": "", 235 "computation_used": "0", 236 "events": [ 237 { 238 "type": "flow.AccountCreated", 239 "transaction_id": "%s", 240 "transaction_index": "1", 241 "event_index": "0", 242 "payload": "%s" 243 } 244 ], 245 "_links": { 246 "_self": "/v1/transaction_results/%s" 247 } 248 }`, bid.String(), cid.String(), id.String(), util.ToBase64(txr.Events[0].Payload), id.String()) 249 250 t.Run("get by transaction ID", func(t *testing.T) { 251 backend := &mock.API{} 252 req := getTransactionResultReq(id.String(), "", "") 253 254 backend.Mock. 255 On("GetTransactionResult", mocks.Anything, id, flow.ZeroID, flow.ZeroID, entities.EventEncodingVersion_JSON_CDC_V0). 256 Return(txr, nil) 257 258 assertOKResponse(t, req, expected, backend) 259 }) 260 261 t.Run("get by block ID", func(t *testing.T) { 262 backend := &mock.API{} 263 264 req := getTransactionResultReq(id.String(), bid.String(), "") 265 266 backend.Mock. 267 On("GetTransactionResult", mocks.Anything, id, bid, flow.ZeroID, entities.EventEncodingVersion_JSON_CDC_V0). 268 Return(txr, nil) 269 270 assertOKResponse(t, req, expected, backend) 271 }) 272 273 t.Run("get by collection ID", func(t *testing.T) { 274 backend := &mock.API{} 275 req := getTransactionResultReq(id.String(), "", cid.String()) 276 277 backend.Mock. 278 On("GetTransactionResult", mocks.Anything, id, flow.ZeroID, cid, entities.EventEncodingVersion_JSON_CDC_V0). 279 Return(txr, nil) 280 281 assertOKResponse(t, req, expected, backend) 282 }) 283 284 t.Run("get execution statuses", func(t *testing.T) { 285 backend := &mock.API{} 286 287 testVectors := map[*access.TransactionResult]string{{ 288 Status: flow.TransactionStatusExpired, 289 ErrorMessage: "", 290 }: string(models.FAILURE_RESULT), { 291 Status: flow.TransactionStatusSealed, 292 ErrorMessage: "cadence runtime exception", 293 }: string(models.FAILURE_RESULT), { 294 Status: flow.TransactionStatusFinalized, 295 ErrorMessage: "", 296 }: string(models.PENDING_RESULT), { 297 Status: flow.TransactionStatusPending, 298 ErrorMessage: "", 299 }: string(models.PENDING_RESULT), { 300 Status: flow.TransactionStatusExecuted, 301 ErrorMessage: "", 302 }: string(models.PENDING_RESULT), { 303 Status: flow.TransactionStatusSealed, 304 ErrorMessage: "", 305 }: string(models.SUCCESS_RESULT)} 306 307 for txResult, err := range testVectors { 308 txResult.BlockID = bid 309 txResult.CollectionID = cid 310 req := getTransactionResultReq(id.String(), "", "") 311 backend.Mock. 312 On("GetTransactionResult", mocks.Anything, id, flow.ZeroID, flow.ZeroID, entities.EventEncodingVersion_JSON_CDC_V0). 313 Return(txResult, nil). 314 Once() 315 316 expectedResp := fmt.Sprintf(`{ 317 "block_id": "%s", 318 "collection_id": "%s", 319 "execution": "%s", 320 "status": "%s", 321 "status_code": 0, 322 "error_message": "%s", 323 "computation_used": "0", 324 "events": [], 325 "_links": { 326 "_self": "/v1/transaction_results/%s" 327 } 328 }`, bid.String(), cid.String(), err, cases.Title(language.English).String(strings.ToLower(txResult.Status.String())), txResult.ErrorMessage, id.String()) 329 assertOKResponse(t, req, expectedResp, backend) 330 } 331 }) 332 333 t.Run("get by ID Invalid", func(t *testing.T) { 334 backend := &mock.API{} 335 336 req := getTransactionResultReq("invalid", "", "") 337 338 expected := `{"code":400, "message":"invalid ID format"}` 339 assertResponse(t, req, http.StatusBadRequest, expected, backend) 340 }) 341 } 342 343 func TestCreateTransaction(t *testing.T) { 344 backend := &mock.API{} 345 346 t.Run("create", func(t *testing.T) { 347 tx := unittest.TransactionBodyFixture() 348 tx.PayloadSignatures = []flow.TransactionSignature{unittest.TransactionSignatureFixture()} 349 tx.Arguments = [][]uint8{} 350 req := createTransactionReq(unittest.CreateSendTxHttpPayload(tx)) 351 352 backend.Mock. 353 On("SendTransaction", mocks.Anything, &tx). 354 Return(nil) 355 356 expected := fmt.Sprintf(` 357 { 358 "id":"%s", 359 "script":"YWNjZXNzKGFsbCkgZnVuIG1haW4oKSB7fQ==", 360 "arguments": [], 361 "reference_block_id":"%s", 362 "gas_limit":"10", 363 "payer":"8c5303eaa26202d6", 364 "proposal_key":{ 365 "address":"8c5303eaa26202d6", 366 "key_index":"1", 367 "sequence_number":"0" 368 }, 369 "authorizers":[ 370 "8c5303eaa26202d6" 371 ], 372 "payload_signatures":[ 373 { 374 "address":"8c5303eaa26202d6", 375 "key_index":"1", 376 "signature":"%s" 377 } 378 ], 379 "envelope_signatures":[ 380 { 381 "address":"8c5303eaa26202d6", 382 "key_index":"1", 383 "signature":"%s" 384 } 385 ], 386 "_expandable": { 387 "result": "/v1/transaction_results/%s" 388 }, 389 "_links":{ 390 "_self":"/v1/transactions/%s" 391 } 392 }`, 393 tx.ID(), tx.ReferenceBlockID, util.ToBase64(tx.PayloadSignatures[0].Signature), util.ToBase64(tx.EnvelopeSignatures[0].Signature), tx.ID(), tx.ID()) 394 assertOKResponse(t, req, expected, backend) 395 }) 396 397 t.Run("post invalid transaction", func(t *testing.T) { 398 tests := []struct { 399 inputField string 400 inputValue string 401 output string 402 }{ 403 {"reference_block_id", "-1", `{"code":400, "message":"invalid reference block ID: invalid ID format"}`}, 404 {"reference_block_id", "", `{"code":400, "message":"reference block not provided"}`}, 405 {"gas_limit", "-1", `{"code":400, "message":"invalid gas limit: value must be an unsigned 64 bit integer"}`}, 406 {"payer", "yo", `{"code":400, "message":"invalid payer: invalid address"}`}, 407 {"proposal_key", "yo", `{"code":400, "message":"request body contains an invalid value for the \"proposal_key\" field (at position 461)"}`}, 408 {"authorizers", "", `{"code":400, "message":"request body contains an invalid value for the \"authorizers\" field (at position 32)"}`}, 409 {"authorizers", "yo", `{"code":400, "message":"request body contains an invalid value for the \"authorizers\" field (at position 34)"}`}, 410 {"envelope_signatures", "", `{"code":400, "message":"request body contains an invalid value for the \"envelope_signatures\" field (at position 75)"}`}, 411 {"payload_signatures", "", `{"code":400, "message":"request body contains an invalid value for the \"payload_signatures\" field (at position 292)"}`}, 412 } 413 414 for _, test := range tests { 415 tx := unittest.TransactionBodyFixture() 416 tx.PayloadSignatures = []flow.TransactionSignature{unittest.TransactionSignatureFixture()} 417 testTx := unittest.CreateSendTxHttpPayload(tx) 418 testTx[test.inputField] = test.inputValue 419 req := createTransactionReq(testTx) 420 421 assertResponse(t, req, http.StatusBadRequest, test.output, backend) 422 } 423 }) 424 } 425 426 func transactionResultFixture(tx flow.Transaction) *access.TransactionResult { 427 cid := unittest.IdentifierFixture() 428 return &access.TransactionResult{ 429 Status: flow.TransactionStatusSealed, 430 StatusCode: 1, 431 Events: []flow.Event{ 432 unittest.EventFixture(flow.EventAccountCreated, 0, 0, tx.ID(), 255), 433 }, 434 ErrorMessage: "", 435 BlockID: tx.ReferenceBlockID, 436 CollectionID: cid, 437 } 438 }