github.com/cycloidio/terraform@v1.1.10-0.20220513142504-76d5c768dc63/command/webbrowser/mock.go (about)

     1  package webbrowser
     2  
     3  import (
     4  	"context"
     5  	"fmt"
     6  	"log"
     7  	"net/http"
     8  	"net/url"
     9  	"sync"
    10  
    11  	"github.com/cycloidio/terraform/httpclient"
    12  )
    13  
    14  // NewMockLauncher creates and returns a mock implementation of Launcher,
    15  // with some special behavior designed for use in unit tests.
    16  //
    17  // See the documentation of MockLauncher itself for more information.
    18  func NewMockLauncher(ctx context.Context) *MockLauncher {
    19  	client := httpclient.New()
    20  	return &MockLauncher{
    21  		Client:  client,
    22  		Context: ctx,
    23  	}
    24  }
    25  
    26  // MockLauncher is a mock implementation of Launcher that has some special
    27  // behavior designed for use in unit tests.
    28  //
    29  // When OpenURL is called, MockLauncher will make an HTTP request to the given
    30  // URL rather than interacting with a "real" browser.
    31  //
    32  // In normal situations it will then return with no further action, but if
    33  // the response to the given URL is either a standard HTTP redirect response
    34  // or includes the custom HTTP header X-Redirect-To then MockLauncher will
    35  // send a follow-up request to that target URL, and continue in this manner
    36  // until it reaches a URL that is not a redirect. (The X-Redirect-To header
    37  // is there so that a server can potentially offer a normal HTML page to
    38  // an actual browser while also giving a next-hop hint for MockLauncher.)
    39  //
    40  // Since MockLauncher is not a full programmable user-agent implementation
    41  // it can't be used for testing of real-world web applications, but it can
    42  // be used for testing against specialized test servers that are written
    43  // with MockLauncher in mind and know how to drive the request flow through
    44  // whatever steps are required to complete the desired test.
    45  //
    46  // All of the actions taken by MockLauncher happen asynchronously in the
    47  // background, to simulate the concurrency of a separate web browser.
    48  // Test code using MockLauncher should provide a context which is cancelled
    49  // when the test completes, to help avoid leaking MockLaunchers.
    50  type MockLauncher struct {
    51  	// Client is the HTTP client that MockLauncher will use to make requests.
    52  	// By default (if you use NewMockLauncher) this is a new client created
    53  	// via httpclient.New, but callers may override it if they need customized
    54  	// behavior for a particular test.
    55  	//
    56  	// Do not use a client that is shared with any other subsystem, because
    57  	// MockLauncher will customize the settings of the given client.
    58  	Client *http.Client
    59  
    60  	// Context can be cancelled in order to abort an OpenURL call before it
    61  	// would naturally complete.
    62  	Context context.Context
    63  
    64  	// Responses is a log of all of the responses recieved from the launcher's
    65  	// requests, in the order requested.
    66  	Responses []*http.Response
    67  
    68  	// done is a waitgroup used internally to signal when the async work is
    69  	// complete, in order to make this mock more convenient to use in tests.
    70  	done sync.WaitGroup
    71  }
    72  
    73  var _ Launcher = (*MockLauncher)(nil)
    74  
    75  // OpenURL is the mock implementation of Launcher, which has the special
    76  // behavior described for type MockLauncher.
    77  func (l *MockLauncher) OpenURL(u string) error {
    78  	// We run our operation in the background because it's supposed to be
    79  	// behaving like a web browser running in a separate process.
    80  	log.Printf("[TRACE] webbrowser.MockLauncher: OpenURL(%q) starting in the background", u)
    81  	l.done.Add(1)
    82  	go func() {
    83  		err := l.openURL(u)
    84  		if err != nil {
    85  			// Can't really do anything with this asynchronously, so we'll
    86  			// just log it so that someone debugging will be able to see it.
    87  			log.Printf("[ERROR] webbrowser.MockLauncher: OpenURL(%q): %s", u, err)
    88  		} else {
    89  			log.Printf("[TRACE] webbrowser.MockLauncher: OpenURL(%q) has concluded", u)
    90  		}
    91  		l.done.Done()
    92  	}()
    93  	return nil
    94  }
    95  
    96  func (l *MockLauncher) openURL(u string) error {
    97  	// We need to disable automatic redirect following so that we can implement
    98  	// it ourselves below, and thus be able to see the redirects in our
    99  	// responses log.
   100  	l.Client.CheckRedirect = func(req *http.Request, via []*http.Request) error {
   101  		return http.ErrUseLastResponse
   102  	}
   103  
   104  	// We'll keep looping as long as the server keeps giving us new URLs to
   105  	// request.
   106  	for u != "" {
   107  		log.Printf("[DEBUG] webbrowser.MockLauncher: requesting %s", u)
   108  		req, err := http.NewRequest("GET", u, nil)
   109  		if err != nil {
   110  			return fmt.Errorf("failed to construct HTTP request for %s: %s", u, err)
   111  		}
   112  		resp, err := l.Client.Do(req)
   113  		if err != nil {
   114  			log.Printf("[DEBUG] webbrowser.MockLauncher: request failed: %s", err)
   115  			return fmt.Errorf("error requesting %s: %s", u, err)
   116  		}
   117  		l.Responses = append(l.Responses, resp)
   118  		if resp.StatusCode >= 400 {
   119  			log.Printf("[DEBUG] webbrowser.MockLauncher: request failed: %s", resp.Status)
   120  			return fmt.Errorf("error requesting %s: %s", u, resp.Status)
   121  		}
   122  		log.Printf("[DEBUG] webbrowser.MockLauncher: request succeeded: %s", resp.Status)
   123  
   124  		u = "" // unless it's a redirect, we'll stop after this
   125  		if location := resp.Header.Get("Location"); location != "" {
   126  			u = location
   127  		} else if redirectTo := resp.Header.Get("X-Redirect-To"); redirectTo != "" {
   128  			u = redirectTo
   129  		}
   130  
   131  		if u != "" {
   132  			// HTTP technically doesn't permit relative URLs in Location, but
   133  			// browsers tolerate it and so real-world servers do it, and thus
   134  			// we'll allow it here too.
   135  			oldURL := resp.Request.URL
   136  			givenURL, err := url.Parse(u)
   137  			if err != nil {
   138  				return fmt.Errorf("invalid redirect URL %s: %s", u, err)
   139  			}
   140  			u = oldURL.ResolveReference(givenURL).String()
   141  			log.Printf("[DEBUG] webbrowser.MockLauncher: redirected to %s", u)
   142  		}
   143  	}
   144  
   145  	log.Printf("[DEBUG] webbrowser.MockLauncher: all done")
   146  	return nil
   147  }
   148  
   149  // Wait blocks until the MockLauncher has finished its asynchronous work of
   150  // making HTTP requests and following redirects, at which point it will have
   151  // reached a request that didn't redirect anywhere and stopped iterating.
   152  func (l *MockLauncher) Wait() {
   153  	log.Printf("[TRACE] webbrowser.MockLauncher: Wait() for current work to complete")
   154  	l.done.Wait()
   155  }