| package webbrowser |
| |
| import ( |
| "context" |
| "fmt" |
| "log" |
| "net/http" |
| "net/url" |
| "sync" |
| |
| "github.com/hashicorp/terraform/internal/httpclient" |
| ) |
| |
| // NewMockLauncher creates and returns a mock implementation of Launcher, |
| // with some special behavior designed for use in unit tests. |
| // |
| // See the documentation of MockLauncher itself for more information. |
| func NewMockLauncher(ctx context.Context) *MockLauncher { |
| client := httpclient.New() |
| return &MockLauncher{ |
| Client: client, |
| Context: ctx, |
| } |
| } |
| |
| // MockLauncher is a mock implementation of Launcher that has some special |
| // behavior designed for use in unit tests. |
| // |
| // When OpenURL is called, MockLauncher will make an HTTP request to the given |
| // URL rather than interacting with a "real" browser. |
| // |
| // In normal situations it will then return with no further action, but if |
| // the response to the given URL is either a standard HTTP redirect response |
| // or includes the custom HTTP header X-Redirect-To then MockLauncher will |
| // send a follow-up request to that target URL, and continue in this manner |
| // until it reaches a URL that is not a redirect. (The X-Redirect-To header |
| // is there so that a server can potentially offer a normal HTML page to |
| // an actual browser while also giving a next-hop hint for MockLauncher.) |
| // |
| // Since MockLauncher is not a full programmable user-agent implementation |
| // it can't be used for testing of real-world web applications, but it can |
| // be used for testing against specialized test servers that are written |
| // with MockLauncher in mind and know how to drive the request flow through |
| // whatever steps are required to complete the desired test. |
| // |
| // All of the actions taken by MockLauncher happen asynchronously in the |
| // background, to simulate the concurrency of a separate web browser. |
| // Test code using MockLauncher should provide a context which is cancelled |
| // when the test completes, to help avoid leaking MockLaunchers. |
| type MockLauncher struct { |
| // Client is the HTTP client that MockLauncher will use to make requests. |
| // By default (if you use NewMockLauncher) this is a new client created |
| // via httpclient.New, but callers may override it if they need customized |
| // behavior for a particular test. |
| // |
| // Do not use a client that is shared with any other subsystem, because |
| // MockLauncher will customize the settings of the given client. |
| Client *http.Client |
| |
| // Context can be cancelled in order to abort an OpenURL call before it |
| // would naturally complete. |
| Context context.Context |
| |
| // Responses is a log of all of the responses recieved from the launcher's |
| // requests, in the order requested. |
| Responses []*http.Response |
| |
| // done is a waitgroup used internally to signal when the async work is |
| // complete, in order to make this mock more convenient to use in tests. |
| done sync.WaitGroup |
| } |
| |
| var _ Launcher = (*MockLauncher)(nil) |
| |
| // OpenURL is the mock implementation of Launcher, which has the special |
| // behavior described for type MockLauncher. |
| func (l *MockLauncher) OpenURL(u string) error { |
| // We run our operation in the background because it's supposed to be |
| // behaving like a web browser running in a separate process. |
| log.Printf("[TRACE] webbrowser.MockLauncher: OpenURL(%q) starting in the background", u) |
| l.done.Add(1) |
| go func() { |
| err := l.openURL(u) |
| if err != nil { |
| // Can't really do anything with this asynchronously, so we'll |
| // just log it so that someone debugging will be able to see it. |
| log.Printf("[ERROR] webbrowser.MockLauncher: OpenURL(%q): %s", u, err) |
| } else { |
| log.Printf("[TRACE] webbrowser.MockLauncher: OpenURL(%q) has concluded", u) |
| } |
| l.done.Done() |
| }() |
| return nil |
| } |
| |
| func (l *MockLauncher) openURL(u string) error { |
| // We need to disable automatic redirect following so that we can implement |
| // it ourselves below, and thus be able to see the redirects in our |
| // responses log. |
| l.Client.CheckRedirect = func(req *http.Request, via []*http.Request) error { |
| return http.ErrUseLastResponse |
| } |
| |
| // We'll keep looping as long as the server keeps giving us new URLs to |
| // request. |
| for u != "" { |
| log.Printf("[DEBUG] webbrowser.MockLauncher: requesting %s", u) |
| req, err := http.NewRequest("GET", u, nil) |
| if err != nil { |
| return fmt.Errorf("failed to construct HTTP request for %s: %s", u, err) |
| } |
| resp, err := l.Client.Do(req) |
| if err != nil { |
| log.Printf("[DEBUG] webbrowser.MockLauncher: request failed: %s", err) |
| return fmt.Errorf("error requesting %s: %s", u, err) |
| } |
| l.Responses = append(l.Responses, resp) |
| if resp.StatusCode >= 400 { |
| log.Printf("[DEBUG] webbrowser.MockLauncher: request failed: %s", resp.Status) |
| return fmt.Errorf("error requesting %s: %s", u, resp.Status) |
| } |
| log.Printf("[DEBUG] webbrowser.MockLauncher: request succeeded: %s", resp.Status) |
| |
| u = "" // unless it's a redirect, we'll stop after this |
| if location := resp.Header.Get("Location"); location != "" { |
| u = location |
| } else if redirectTo := resp.Header.Get("X-Redirect-To"); redirectTo != "" { |
| u = redirectTo |
| } |
| |
| if u != "" { |
| // HTTP technically doesn't permit relative URLs in Location, but |
| // browsers tolerate it and so real-world servers do it, and thus |
| // we'll allow it here too. |
| oldURL := resp.Request.URL |
| givenURL, err := url.Parse(u) |
| if err != nil { |
| return fmt.Errorf("invalid redirect URL %s: %s", u, err) |
| } |
| u = oldURL.ResolveReference(givenURL).String() |
| log.Printf("[DEBUG] webbrowser.MockLauncher: redirected to %s", u) |
| } |
| } |
| |
| log.Printf("[DEBUG] webbrowser.MockLauncher: all done") |
| return nil |
| } |
| |
| // Wait blocks until the MockLauncher has finished its asynchronous work of |
| // making HTTP requests and following redirects, at which point it will have |
| // reached a request that didn't redirect anywhere and stopped iterating. |
| func (l *MockLauncher) Wait() { |
| log.Printf("[TRACE] webbrowser.MockLauncher: Wait() for current work to complete") |
| l.done.Wait() |
| } |