| 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() | |
| } |