@ 261101.7
current
package main

import (
	"bytes"
	"encoding/binary"
	"fmt"
	"net"
	"os"
	"path/filepath"
	"strings"
	"syscall"
	"testing"
	"time"
)

// TestMain keeps the tests' entries out of the user's journal and state
// directory.
func TestMain(m *testing.M) {
	dir, err := os.MkdirTemp("", progName+"-test-")
	if err != nil {
		panic(err)
	}
	os.Setenv("XDG_STATE_HOME", dir)
	journalSocket = filepath.Join(dir, "no-journal")
	journalStream = filepath.Join(dir, "no-journal-stream")
	code := m.Run()
	os.RemoveAll(dir)
	os.Exit(code)
}

// useJournal points the logger at a fresh socket and returns what reaches
// it; with sock "" it points at none, so entries go to the file.
func useJournal(t *testing.T, sock string) {
	t.Helper()
	old := journalSocket
	logger.mu.Lock()
	if logger.conn != nil {
		logger.conn.Close()
	}
	logger.conn, logger.tried, logger.ep = nil, false, ""
	journalSocket = sock
	if sock == "" {
		journalSocket = filepath.Join(t.TempDir(), "none")
	}
	logger.mu.Unlock()
	t.Cleanup(func() {
		logger.mu.Lock()
		if logger.conn != nil {
			logger.conn.Close()
		}
		logger.conn, logger.tried, logger.ep = nil, false, ""
		journalSocket = old
		logger.mu.Unlock()
	})
}

func TestJournalEntry(t *testing.T) {
	sock := filepath.Join(t.TempDir(), "socket")
	pc, err := net.ListenUnixgram("unixgram", &net.UnixAddr{Name: sock, Net: "unixgram"})
	if err != nil {
		t.Skip("no unix datagram sockets:", err)
	}
	defer pc.Close()
	useJournal(t, sock)
	setLogEndpoint("127.31.1.1-80")
	logErr("E5: git: exit 1\nline two")

	pc.SetReadDeadline(time.Now().Add(2 * time.Second))
	buf := make([]byte, 65536)
	n, err := pc.Read(buf)
	if err != nil {
		t.Fatal(err)
	}
	got := buf[:n]
	for _, want := range []string{"PRIORITY=3\n", "SYSLOG_IDENTIFIER=" + progName + "\n", envPrefix + "ENDPOINT=127.31.1.1-80\n"} {
		if !bytes.Contains(got, []byte(want)) {
			t.Errorf("entry lacks %q:\n%q", want, got)
		}
	}
	// A message with a newline goes as its length and bytes.
	msg := "E5: git: exit 1\nline two"
	var want bytes.Buffer
	want.WriteString("MESSAGE\n")
	binary.Write(&want, binary.LittleEndian, uint64(len(msg)))
	want.WriteString(msg + "\n")
	if !bytes.HasPrefix(got, want.Bytes()) {
		t.Errorf("message encoded as %q, want %q", got, want.Bytes())
	}
}

func TestLogFileFallback(t *testing.T) {
	useJournal(t, "")
	t.Setenv("XDG_STATE_HOME", t.TempDir())
	setLogEndpoint("127.31.1.1-8080")
	logInfo("server starts\nsecond line")
	path, _ := logFile()
	b, err := os.ReadFile(path)
	if err != nil {
		t.Fatal(err)
	}
	line := string(b)
	if !strings.Contains(line, " 127.31.1.1-8080 info server starts\n    second line\n") {
		t.Errorf("log line %q", line)
	}
	if info, _ := os.Stat(path); info.Mode().Perm() != 0o600 {
		t.Errorf("log mode %o, want 600", info.Mode().Perm())
	}
}

// The file rotates once at maxLogFile, and the next entry starts a new one.
func TestLogFileRotates(t *testing.T) {
	useJournal(t, "")
	t.Setenv("XDG_STATE_HOME", t.TempDir())
	path, _ := logFile()
	os.MkdirAll(filepath.Dir(path), 0o700)
	os.WriteFile(path, bytes.Repeat([]byte("x"), maxLogFile), 0o600)
	logInfo("after")
	if info, err := os.Stat(path + ".1"); err != nil || info.Size() != maxLogFile {
		t.Fatalf("log.1: %v %v", info, err)
	}
	if b, _ := os.ReadFile(path); !strings.HasSuffix(string(b), " info after\n") || len(b) > 200 {
		t.Errorf("new log %q", b)
	}
}

func TestCommandLine(t *testing.T) {
	got := commandLine([]string{"--no-open", "--share", "--secret=abc", "my docs", "it's"})
	want := progName + ` --no-open --share --secret=<token> 'my docs' 'it'\''s'`
	if got != want {
		t.Errorf("got %s, want %s", got, want)
	}
}

// Requests are logged but for the ones each page brings along, and a
// secret tree's token stays out.
func TestRequestLog(t *testing.T) {
	useJournal(t, "")
	t.Setenv("XDG_STATE_HOME", t.TempDir())
	s, tr, _ := serveTree(t)
	h := s.handler()
	for _, p := range []string{styleURL, "/favicon.ico", ownPath + "live?path=/", "/index.md"} {
		get(h, p)
	}
	tr.prefix, tr.secret = "/s3cr3t/", true
	get(h, "/s3cr3t/index.md?raw")
	get(h, "/s3cr3t?log")
	// Escapes spell the token another way.
	get(h, "/%733cr3t/index.md?raw")
	get(h, "/%733cr3t?log")

	path, _ := logFile()
	b, _ := os.ReadFile(path)
	log := string(b)
	for _, skip := range []string{"style.", "favicon", ownPath + "live", "s3cr3t", "3cr3t"} {
		if strings.Contains(log, skip) {
			t.Errorf("log holds %s:\n%s", skip, log)
		}
	}
	if !strings.Contains(log, " info GET /index.md 200 ") || !strings.Contains(log, " info GET /<token>/index.md?raw 200 ") {
		t.Errorf("log lacks the page requests:\n%s", log)
	}
	if !strings.Contains(log, " info GET /<token>?log 302 ") {
		t.Errorf("log lacks the bare secret path:\n%s", log)
	}
}

// A shared server logs who asked.
func TestRequestLogShared(t *testing.T) {
	useJournal(t, "")
	t.Setenv("XDG_STATE_HOME", t.TempDir())
	s, tr, _ := serveTree(t)
	s.shared, tr.shared = true, true
	get(s.handler(), "/index.md")
	path, _ := logFile()
	b, _ := os.ReadFile(path)
	if !strings.Contains(string(b), " from 192.0.2.1:1234\n") {
		t.Errorf("shared log lacks the visitor:\n%s", b)
	}
}

// Under systemd, stderr is the journal, so what is logged is not printed
// as well.
func TestStderrIsJournal(t *testing.T) {
	var st syscall.Stat_t
	if err := syscall.Fstat(2, &st); err != nil {
		t.Skip(err)
	}
	t.Setenv("JOURNAL_STREAM", fmt.Sprintf("%d:%d", st.Dev, st.Ino))
	if !stderrIsJournal() {
		t.Error("stderr's own device and inode are not the journal")
	}
	t.Setenv("JOURNAL_STREAM", fmt.Sprintf("%d:%d", st.Dev, st.Ino+1))
	if stderrIsJournal() {
		t.Error("another inode is the journal")
	}
	t.Setenv("JOURNAL_STREAM", "")
	if stderrIsJournal() {
		t.Error("no JOURNAL_STREAM is the journal")
	}
}