| // Copyright (c) The OpenTofu Authors | |
| // SPDX-License-Identifier: MPL-2.0 | |
| // Copyright (c) 2023 HashiCorp, Inc. | |
| // SPDX-License-Identifier: MPL-2.0 | |
| package webbrowser | |
| import ( | |
| "fmt" | |
| "os" | |
| "path/filepath" | |
| "runtime" | |
| "testing" | |
| ) | |
| const fakeBrowserLaunchCmdOutputEnvName = "OPENTOFU_WEBBROWSER_EXEC_TEST_OUTPUT" | |
| // TestMain overrides the test entrypoint so that we can reuse the test | |
| // executable as a fake browser launcher command when testing | |
| // [NewExecLauncher]. | |
| func TestMain(m *testing.M) { | |
| if f := os.Getenv(fakeBrowserLaunchCmdOutputEnvName); f != "" { | |
| err := fakeBrowserLauncherCommand(f) | |
| if err != nil { | |
| fmt.Fprintf(os.Stderr, "fake browser launcher failed: %s", err) | |
| os.Exit(1) | |
| } | |
| os.Exit(0) | |
| } | |
| os.Exit(m.Run()) | |
| } | |
| func fakeBrowserLauncherCommand(outputFilename string) error { | |
| // The "exec" browser launcher must pass the URL to open in the | |
| // first argument to the executable it launches. | |
| url := os.Args[1] | |
| return os.WriteFile(outputFilename, []byte(url), os.ModePerm) | |
| } | |
| func TestExecLauncher(t *testing.T) { | |
| // For this test we re-use the text executable as a fake browser-launching | |
| // program, through the special logic in [TestMain]. | |
| fakeExec := os.Args[0] | |
| tempDir := t.TempDir() | |
| outputFile := filepath.Join(tempDir, "browser-exec-launcher-test") | |
| t.Setenv(fakeBrowserLaunchCmdOutputEnvName, outputFile) | |
| launcher := NewExecLauncher(fakeExec) | |
| err := launcher.OpenURL("http://example.com/") | |
| if err != nil { | |
| t.Fatal(err) | |
| } | |
| result, err := os.ReadFile(outputFile) | |
| if err != nil { | |
| t.Fatal(err) | |
| } | |
| if got, want := string(result), "http://example.com/"; got != want { | |
| t.Errorf("wrong URL written to output file\ngot: %s\nwant: %s", got, want) | |
| } | |
| } | |
| func TestParseBrowserEnv_success(t *testing.T) { | |
| // ParseBrowserEnv only actually needs to work on Unix-like systems, so | |
| // the test scenario below is not written to be portable. | |
| // There's no runtime equivalent of the "unix" build tag, so we just | |
| // explicitly test the two main Unix OSes we support here. | |
| if runtime.GOOS != "linux" && runtime.GOOS != "darwin" { | |
| t.Skip("ParseBrowserEnv is only for unix systems") | |
| } | |
| tmpDir := t.TempDir() | |
| fakeExec := filepath.Join(tmpDir, "fake-launch-browser") | |
| err := os.WriteFile(fakeExec, []byte(`not a real program`), 0755) | |
| if err != nil { | |
| // NOTE: This test requires the temp directory to be somewhere that | |
| // allows executables, so this won't work if the temp directory is | |
| // on a "noexec" mount on a Unix-style system. | |
| t.Fatal(err) | |
| } | |
| // Temporarily we'll reset the search path to just our temp directory | |
| t.Setenv("PATH", tmpDir) | |
| result := ParseBrowserEnv("fake-launch-browser") | |
| if result == "" { | |
| t.Fatal("failed to find fake executable") | |
| } | |
| if got, want := filepath.Base(result), "fake-launch-browser"; got != want { | |
| t.Fatalf("returned path %q has wrong basename %q; want %q", result, got, want) | |
| } | |
| } | |
| func TestParseBrowserEnv_empty(t *testing.T) { | |
| result := ParseBrowserEnv("") | |
| if result != "" { | |
| t.Errorf("returned %q, but wanted empty string", result) | |
| } | |
| } | |
| func TestParseBrowserEnv_esrComplexSpec(t *testing.T) { | |
| // The following tests with a strings following the more complex | |
| // interpretation of BROWSER from http://www.catb.org/~esr/BROWSER/ , which | |
| // OpenTofu intentionally doesn't support and so should be treated as | |
| // if the environment variable isn't set at all. | |
| t.Run(`with %s`, func(t *testing.T) { | |
| // The esr proposal calls for checking whether there's a %s sequence | |
| // in the value and then, if so, substituting the URL there and then | |
| // passing the entire result to a shell. This is the main thing that | |
| // different implementations did inconsistently, because it's | |
| // unspecified whether the %s should be placed in quotes in the | |
| // environment variable, if those quotes should be inserted by the | |
| // program acting on the variable, or if some other shell escaping | |
| // strategy should be used instead. We just ignore this form entirely | |
| // because it's apparently not commonly used and it's unclear how | |
| // to implement it without causing security problems. | |
| result := ParseBrowserEnv("example %s") | |
| if result != "" { | |
| t.Errorf("returned %q, but wanted empty string", result) | |
| } | |
| }) | |
| t.Run("multiple commands", func(t *testing.T) { | |
| // The esr proposal calls for splitting the string on semicolon | |
| // and trying one command at a time until one succeeds. That's | |
| // ambiguous with there being a single command whose path contains | |
| // a semicolon, so we just try to treat it as a single command and | |
| // ignore the value if that doesn't work. In practice the need for | |
| // multiple options to try tends to be met instead by setting BROWSER | |
| // to refer to a wrapper script that deals with the selection policy, | |
| // which is the pattern OpenTofu supports. | |
| result := ParseBrowserEnv("example1;example2") | |
| if result != "" { | |
| t.Errorf("returned %q, but wanted empty string", result) | |
| } | |
| }) | |
| } |