opentofu/internal/command/webbrowser/mock.go
Martin Atkins ffe056bacb Move command/ to internal/command/
This is part of a general effort to move all of Terraform's non-library
package surface under internal in order to reinforce that these are for
internal use within Terraform only.

If you were previously importing packages under this prefix into an
external codebase, you could pin to an earlier release tag as an interim
solution until you've make a plan to achieve the same functionality some
other way.
2021-05-17 14:09:07 -07:00

156 lines
5.8 KiB
Go

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