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  }