github.com/graywolf-at-work-2/terraform-vendor@v1.4.5/internal/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/hashicorp/terraform/internal/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 }