2019-08-29 17:50:03 -05:00
|
|
|
package webbrowser
|
|
|
|
|
|
|
|
import (
|
|
|
|
"context"
|
|
|
|
"fmt"
|
|
|
|
"log"
|
|
|
|
"net/http"
|
|
|
|
"net/url"
|
|
|
|
"sync"
|
|
|
|
|
2021-05-17 11:54:53 -05:00
|
|
|
"github.com/hashicorp/terraform/internal/httpclient"
|
2019-08-29 17:50:03 -05:00
|
|
|
)
|
|
|
|
|
|
|
|
// 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()
|
|
|
|
}
|