github.com/rstandt/terraform@v0.12.32-0.20230710220336-b1063613405c/internal/getproviders/registry_client_test.go (about) 1 package getproviders 2 3 import ( 4 "log" 5 "net/http" 6 "net/http/httptest" 7 "strings" 8 "testing" 9 10 svchost "github.com/hashicorp/terraform-svchost" 11 disco "github.com/hashicorp/terraform-svchost/disco" 12 ) 13 14 // testServices starts up a local HTTP server running a fake provider registry 15 // service and returns a service discovery object pre-configured to consider 16 // the host "example.com" to be served by the fake registry service. 17 // 18 // The returned discovery object also knows the hostname "not.example.com" 19 // which does not have a provider registry at all and "too-new.example.com" 20 // which has a "providers.v99" service that is inoperable but could be useful 21 // to test the error reporting for detecting an unsupported protocol version. 22 // It also knows fails.example.com but it refers to an endpoint that doesn't 23 // correctly speak HTTP, to simulate a protocol error. 24 // 25 // The second return value is a function to call at the end of a test function 26 // to shut down the test server. After you call that function, the discovery 27 // object becomes useless. 28 func testServices(t *testing.T) (services *disco.Disco, baseURL string, cleanup func()) { 29 server := httptest.NewServer(http.HandlerFunc(fakeRegistryHandler)) 30 31 services = disco.New() 32 services.ForceHostServices(svchost.Hostname("example.com"), map[string]interface{}{ 33 "providers.v1": server.URL + "/providers/v1/", 34 }) 35 services.ForceHostServices(svchost.Hostname("not.example.com"), map[string]interface{}{}) 36 services.ForceHostServices(svchost.Hostname("too-new.example.com"), map[string]interface{}{ 37 // This service doesn't actually work; it's here only to be 38 // detected as "too new" by the discovery logic. 39 "providers.v99": server.URL + "/providers/v99/", 40 }) 41 services.ForceHostServices(svchost.Hostname("fails.example.com"), map[string]interface{}{ 42 "providers.v1": server.URL + "/fails-immediately/", 43 }) 44 45 // We'll also permit registry.terraform.io here just because it's our 46 // default and has some unique features that are not allowed on any other 47 // hostname. It behaves the same as example.com, which should be preferred 48 // if you're not testing something specific to the default registry in order 49 // to ensure that most things are hostname-agnostic. 50 services.ForceHostServices(svchost.Hostname("registry.terraform.io"), map[string]interface{}{ 51 "providers.v1": server.URL + "/providers/v1/", 52 }) 53 54 return services, server.URL, func() { 55 server.Close() 56 } 57 } 58 59 // testRegistrySource is a wrapper around testServices that uses the created 60 // discovery object to produce a Source instance that is ready to use with the 61 // fake registry services. 62 // 63 // As with testServices, the second return value is a function to call at the end 64 // of your test in order to shut down the test server. 65 func testRegistrySource(t *testing.T) (source *RegistrySource, baseURL string, cleanup func()) { 66 services, baseURL, close := testServices(t) 67 source = NewRegistrySource(services) 68 return source, baseURL, close 69 } 70 71 func fakeRegistryHandler(resp http.ResponseWriter, req *http.Request) { 72 path := req.URL.EscapedPath() 73 if strings.HasPrefix(path, "/fails-immediately/") { 74 // Here we take over the socket and just close it immediately, to 75 // simulate one possible way a server might not be an HTTP server. 76 hijacker, ok := resp.(http.Hijacker) 77 if !ok { 78 // Not hijackable, so we'll just fail normally. 79 // If this happens, tests relying on this will fail. 80 resp.WriteHeader(500) 81 resp.Write([]byte(`cannot hijack`)) 82 return 83 } 84 conn, _, err := hijacker.Hijack() 85 if err != nil { 86 resp.WriteHeader(500) 87 resp.Write([]byte(`hijack failed`)) 88 return 89 } 90 conn.Close() 91 return 92 } 93 94 if !strings.HasPrefix(path, "/providers/v1/") { 95 resp.WriteHeader(404) 96 resp.Write([]byte(`not a provider registry endpoint`)) 97 return 98 } 99 100 pathParts := strings.Split(path, "/")[3:] 101 if len(pathParts) < 2 { 102 resp.WriteHeader(404) 103 resp.Write([]byte(`unexpected number of path parts`)) 104 return 105 } 106 log.Printf("[TRACE] fake provider registry request for %#v", pathParts) 107 if len(pathParts) == 2 { 108 switch pathParts[0] + "/" + pathParts[1] { 109 110 case "-/legacy": 111 // NOTE: This legacy lookup endpoint is specific to 112 // registry.terraform.io and not expected to work on any other 113 // registry host. 114 resp.Header().Set("Content-Type", "application/json") 115 resp.WriteHeader(200) 116 resp.Write([]byte(`{"namespace":"legacycorp"}`)) 117 118 default: 119 resp.WriteHeader(404) 120 resp.Write([]byte(`unknown namespace or provider type for direct lookup`)) 121 } 122 } 123 124 if len(pathParts) < 3 { 125 resp.WriteHeader(404) 126 resp.Write([]byte(`unexpected number of path parts`)) 127 return 128 } 129 130 if pathParts[2] == "versions" { 131 if len(pathParts) != 3 { 132 resp.WriteHeader(404) 133 resp.Write([]byte(`extraneous path parts`)) 134 return 135 } 136 137 switch pathParts[0] + "/" + pathParts[1] { 138 case "awesomesauce/happycloud": 139 resp.Header().Set("Content-Type", "application/json") 140 resp.WriteHeader(200) 141 // Note that these version numbers are intentionally misordered 142 // so we can test that the client-side code places them in the 143 // correct order (lowest precedence first). 144 resp.Write([]byte(`{"versions":[{"version":"1.2.0"}, {"version":"1.0.0"}]}`)) 145 case "weaksauce/no-versions": 146 resp.Header().Set("Content-Type", "application/json") 147 resp.WriteHeader(200) 148 resp.Write([]byte(`{"versions":[]}`)) 149 default: 150 resp.WriteHeader(404) 151 resp.Write([]byte(`unknown namespace or provider type`)) 152 } 153 return 154 } 155 156 if len(pathParts) == 6 && pathParts[3] == "download" { 157 switch pathParts[0] + "/" + pathParts[1] { 158 case "awesomesauce/happycloud": 159 if pathParts[4] == "nonexist" { 160 resp.WriteHeader(404) 161 resp.Write([]byte(`unsupported OS`)) 162 return 163 } 164 resp.Header().Set("Content-Type", "application/json") 165 resp.WriteHeader(200) 166 // Note that these version numbers are intentionally misordered 167 // so we can test that the client-side code places them in the 168 // correct order (lowest precedence first). 169 resp.Write([]byte(`{"protocols":["5.0"],"os":"` + pathParts[4] + `","arch":"` + pathParts[5] + `","filename":"happycloud_` + pathParts[2] + `.zip","download_url":"/pkg/happycloud_` + pathParts[2] + `.zip","shasum":"000000000000000000000000000000000000000000000000000000000000f00d"}`)) 170 default: 171 resp.WriteHeader(404) 172 resp.Write([]byte(`unknown namespace/provider/version/architecture`)) 173 } 174 return 175 } 176 177 resp.WriteHeader(404) 178 resp.Write([]byte(`unrecognized path scheme`)) 179 }