github.com/MetalBlockchain/metalgo@v1.11.9/tests/fixture/test_data_server.go (about) 1 // Copyright (C) 2019-2024, Ava Labs, Inc. All rights reserved. 2 // See the file LICENSE for licensing terms. 3 4 package fixture 5 6 import ( 7 "context" 8 "encoding/json" 9 "errors" 10 "fmt" 11 "io" 12 "net" 13 "net/http" 14 "net/url" 15 "strconv" 16 "strings" 17 "sync" 18 "time" 19 20 "github.com/MetalBlockchain/metalgo/utils" 21 "github.com/MetalBlockchain/metalgo/utils/crypto/secp256k1" 22 ) 23 24 const ( 25 allocateKeysPath = "/allocateKeys" 26 keyCountParameterName = "count" 27 requestedKeyCountExceedsAvailable = "requested key count exceeds available allocation" 28 ) 29 30 var ( 31 errRequestedKeyCountExceedsAvailable = errors.New(requestedKeyCountExceedsAvailable) 32 errInvalidKeyCount = errors.New("key count must be greater than zero") 33 ) 34 35 type TestData struct { 36 PreFundedKeys []*secp256k1.PrivateKey 37 } 38 39 // http server allocating resources to tests potentially executing in parallel 40 type testDataServer struct { 41 // Synchronizes access to test data 42 lock sync.Mutex 43 TestData 44 } 45 46 // Type used to marshal/unmarshal a set of test keys for transmission over http. 47 type keysDocument struct { 48 Keys []*secp256k1.PrivateKey `json:"keys"` 49 } 50 51 func (s *testDataServer) allocateKeys(w http.ResponseWriter, r *http.Request) { 52 // Attempt to parse the count parameter 53 rawKeyCount := r.URL.Query().Get(keyCountParameterName) 54 if len(rawKeyCount) == 0 { 55 msg := fmt.Sprintf("missing %q parameter", keyCountParameterName) 56 http.Error(w, msg, http.StatusBadRequest) 57 return 58 } 59 keyCount, err := strconv.Atoi(rawKeyCount) 60 if err != nil { 61 msg := fmt.Sprintf("unable to parse %q parameter: %v", keyCountParameterName, err) 62 http.Error(w, msg, http.StatusBadRequest) 63 return 64 } 65 66 // Ensure a key will be allocated at most once 67 s.lock.Lock() 68 defer s.lock.Unlock() 69 70 // Only fulfill requests for available keys 71 if keyCount > len(s.PreFundedKeys) { 72 http.Error(w, requestedKeyCountExceedsAvailable, http.StatusInternalServerError) 73 return 74 } 75 76 // Allocate the requested number of keys 77 remainingKeys := len(s.PreFundedKeys) - keyCount 78 allocatedKeys := s.PreFundedKeys[remainingKeys:] 79 80 keysDoc := &keysDocument{ 81 Keys: allocatedKeys, 82 } 83 if err := json.NewEncoder(w).Encode(keysDoc); err != nil { 84 msg := fmt.Sprintf("failed to encode test keys: %v", err) 85 http.Error(w, msg, http.StatusInternalServerError) 86 return 87 } 88 89 // Forget the allocated keys 90 utils.ZeroSlice(allocatedKeys) 91 s.PreFundedKeys = s.PreFundedKeys[:remainingKeys] 92 } 93 94 // Serve test data via http to ensure allocation is synchronized even when 95 // ginkgo specs are executing in parallel. Returns the URI to access the server. 96 func ServeTestData(testData TestData) (string, error) { 97 // Listen on a dynamic port to avoid conflicting with other applications 98 listener, err := net.Listen("tcp", "127.0.0.1:0") 99 if err != nil { 100 return "", fmt.Errorf("failed to initialize listener for test data server: %w", err) 101 } 102 address := fmt.Sprintf("http://%s", listener.Addr()) 103 104 s := &testDataServer{ 105 TestData: testData, 106 } 107 mux := http.NewServeMux() 108 mux.HandleFunc(allocateKeysPath, s.allocateKeys) 109 110 httpServer := &http.Server{ 111 Handler: mux, 112 ReadHeaderTimeout: 3 * time.Second, 113 } 114 115 go func() { 116 // Serve always returns a non-nil error and closes l. 117 if err := httpServer.Serve(listener); err != http.ErrServerClosed { 118 panic(fmt.Sprintf("unexpected error closing test data server: %v", err)) 119 } 120 }() 121 122 return address, nil 123 } 124 125 // Retrieve the specified number of pre-funded test keys from the provided URI. A given 126 // key is allocated at most once during the life of the test data server. 127 func AllocatePreFundedKeys(baseURI string, count int) ([]*secp256k1.PrivateKey, error) { 128 if count <= 0 { 129 return nil, errInvalidKeyCount 130 } 131 132 uri, err := url.Parse(baseURI) 133 if err != nil { 134 return nil, fmt.Errorf("failed to parse uri: %w", err) 135 } 136 uri.RawQuery = url.Values{ 137 keyCountParameterName: {strconv.Itoa(count)}, 138 }.Encode() 139 uri.Path = allocateKeysPath 140 req, err := http.NewRequestWithContext(context.Background(), http.MethodGet, uri.String(), nil) 141 if err != nil { 142 return nil, fmt.Errorf("failed to construct request: %w", err) 143 } 144 145 resp, err := http.DefaultClient.Do(req) 146 if err != nil { 147 return nil, fmt.Errorf("failed to request pre-funded keys: %w", err) 148 } 149 defer resp.Body.Close() 150 151 body, err := io.ReadAll(resp.Body) 152 if err != nil { 153 return nil, fmt.Errorf("failed to read response for pre-funded keys: %w", err) 154 } 155 if resp.StatusCode != http.StatusOK { 156 if strings.TrimSpace(string(body)) == requestedKeyCountExceedsAvailable { 157 return nil, errRequestedKeyCountExceedsAvailable 158 } 159 return nil, fmt.Errorf("test data server returned unexpected status code %d: %v", resp.StatusCode, body) 160 } 161 162 keysDoc := &keysDocument{} 163 if err := json.Unmarshal(body, keysDoc); err != nil { 164 return nil, fmt.Errorf("failed to unmarshal pre-funded keys: %w", err) 165 } 166 return keysDoc.Keys, nil 167 }