From 80429b97e58d00b68b7773937c35b7ae6927af03 Mon Sep 17 00:00:00 2001 From: Brad Fitzpatrick Date: Thu, 29 Apr 2021 14:44:08 -0700 Subject: [PATCH] testing: add start of an integration test Only minimal tailscale + tailscaled for now. And a super minimal in-memory logcatcher. No control ... yet. Signed-off-by: Brad Fitzpatrick --- tstest/integration/integration_test.go | 175 +++++++++++++++++++++++++ 1 file changed, 175 insertions(+) create mode 100644 tstest/integration/integration_test.go diff --git a/tstest/integration/integration_test.go b/tstest/integration/integration_test.go new file mode 100644 index 000000000..01d68d44d --- /dev/null +++ b/tstest/integration/integration_test.go @@ -0,0 +1,175 @@ +// Copyright (c) 2021 Tailscale Inc & AUTHORS All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// Package integration contains Tailscale integration tests. +package integration + +import ( + "bytes" + "encoding/json" + "fmt" + "io" + "io/ioutil" + "log" + "net/http" + "net/http/httptest" + "os" + "os/exec" + "path" + "path/filepath" + "runtime" + "strings" + "sync" + "testing" + "time" + + "go4.org/mem" + "tailscale.com/smallzstd" + "tailscale.com/tstest" +) + +func TestIntegration(t *testing.T) { + if runtime.GOOS == "windows" { + t.Skip("not tested/working on Windows yet") + } + td := t.TempDir() + daemonExe := build(t, td, "tailscale.com/cmd/tailscaled") + cliExe := build(t, td, "tailscale.com/cmd/tailscale") + + logc := new(logCatcher) + ts := httptest.NewServer(logc) + defer ts.Close() + + socketPath := filepath.Join(td, "tailscale.sock") + dcmd := exec.Command(daemonExe, + "--tun=userspace-networking", + "--state="+filepath.Join(td, "tailscale.state"), + "--socket="+socketPath, + ) + dcmd.Env = append(os.Environ(), "TS_LOG_TARGET="+ts.URL) + if err := dcmd.Start(); err != nil { + t.Fatalf("starting tailscaled: %v", err) + } + defer dcmd.Process.Kill() + + var json []byte + if err := tstest.WaitFor(20*time.Second, func() (err error) { + json, err = exec.Command(cliExe, "--socket="+socketPath, "status", "--json").CombinedOutput() + if err != nil { + return fmt.Errorf("running tailscale status: %v, %s", err, json) + } + return nil + }); err != nil { + t.Fatal(err) + } + + if err := tstest.WaitFor(20*time.Second, func() error { + const sub = `Program starting: ` + if !logc.logsContains(mem.S(sub)) { + return fmt.Errorf("log catcher didn't see %#q; got %s", sub, logc.logsString()) + } + return nil + }); err != nil { + t.Error(err) + } + + dcmd.Process.Signal(os.Interrupt) + + ps, err := dcmd.Process.Wait() + if err != nil { + t.Fatalf("tailscaled Wait: %v", err) + } + if ps.ExitCode() != 0 { + t.Errorf("tailscaled ExitCode = %d; want 0", ps.ExitCode()) + } + + t.Logf("number of HTTP logcatcher requests: %v", logc.numRequests()) +} + +func build(t *testing.T, outDir, target string) string { + exe := "" + if runtime.GOOS == "windows" { + exe = ".exe" + } + bin := filepath.Join(outDir, path.Base(target)) + exe + errOut, err := exec.Command("go", "build", "-o", bin, target).CombinedOutput() + if err != nil { + t.Fatalf("failed to build %v: %v, %s", target, err, errOut) + } + return bin +} + +type logCatcher struct { + mu sync.Mutex + buf bytes.Buffer + gotErr error + reqs int +} + +func (lc *logCatcher) logsContains(sub mem.RO) bool { + lc.mu.Lock() + defer lc.mu.Unlock() + return mem.Contains(mem.B(lc.buf.Bytes()), sub) +} + +func (lc *logCatcher) numRequests() int { + lc.mu.Lock() + defer lc.mu.Unlock() + return lc.reqs +} + +func (lc *logCatcher) logsString() string { + lc.mu.Lock() + defer lc.mu.Unlock() + return lc.buf.String() +} + +func (lc *logCatcher) ServeHTTP(w http.ResponseWriter, r *http.Request) { + var body io.Reader = r.Body + if r.Header.Get("Content-Encoding") == "zstd" { + var err error + body, err = smallzstd.NewDecoder(body) + if err != nil { + log.Printf("bad caught zstd: %v", err) + http.Error(w, err.Error(), 400) + return + } + } + bodyBytes, _ := ioutil.ReadAll(body) + + type Entry struct { + Logtail struct { + ClientTime time.Time `json:"client_time"` + ServerTime time.Time `json:"server_time"` + Error struct { + BadData string `json:"bad_data"` + } `json:"error"` + } `json:"logtail"` + Text string `json:"text"` + } + var jreq []Entry + var err error + if len(bodyBytes) > 0 && bodyBytes[0] == '[' { + err = json.Unmarshal(bodyBytes, &jreq) + } else { + var ent Entry + err = json.Unmarshal(bodyBytes, &ent) + jreq = append(jreq, ent) + } + + lc.mu.Lock() + defer lc.mu.Unlock() + lc.reqs++ + if lc.gotErr == nil && err != nil { + lc.gotErr = err + } + if err != nil { + fmt.Fprintf(&lc.buf, "error from %s of %#q: %v\n", r.Method, bodyBytes, err) + } else { + for _, ent := range jreq { + fmt.Fprintf(&lc.buf, "%s\n", strings.TrimSpace(ent.Text)) + } + } + w.WriteHeader(200) // must have no content, but not a 204 +}