xetup/internal/report/report_test.go
X9 Dev ef56410139
All checks were successful
release / build-and-release (push) Successful in 35s
fix(report): footer advertises log attachment only when actually attached
buildHTML ran before zipLog, so the email footer always claimed
"log v priloze (<host>-Deploy.log.zip)" even when the log was unreadable
and no attachment was added. Zip the log first and pass the attachment
name into buildHTML; footer now names the zip only when present, otherwise
points solely to the local copy. Covered by TestBuildHTMLFooterMatchesAttachment.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-28 18:03:45 +02:00

170 lines
5.6 KiB
Go

package report
import (
"archive/zip"
"bytes"
"encoding/base64"
"io"
"mime"
"mime/multipart"
"net/mail"
"os"
"path/filepath"
"strings"
"testing"
"time"
)
var sampleResults = []StepResult{
{Num: "01", Name: "Bloatware", Status: "OK", Elapsed: 3 * time.Second},
{Num: "08", Name: "Activation", Status: "ERROR", Elapsed: 2 * time.Second},
}
// writeLog writes a temp Deploy.log and returns its path.
func writeLog(t *testing.T, content []byte) string {
t.Helper()
p := filepath.Join(t.TempDir(), "Deploy.log")
if err := os.WriteFile(p, content, 0644); err != nil {
t.Fatal(err)
}
return p
}
// buildHTML must embed the direct issue link and the per-machine tracker filter.
func TestBuildHTMLLinks(t *testing.T) {
issueURL := "https://git.xetup.x9.cz/x9/xetup-runs/issues/42"
html := buildHTML(sampleResults, "PC-TEST", "2026-07-28 10:00", issueURL, "PC-TEST-Deploy.log.zip")
if !strings.Contains(html, issueURL) {
t.Errorf("html does not link the run issue %q", issueURL)
}
if !strings.Contains(html, "xetup-runs/issues?q=PC-TEST") {
t.Errorf("html missing per-machine tracker filter link; got:\n%s", html)
}
}
// With no issue (clean run / post failed), only the tracker link is shown.
func TestBuildHTMLNoIssue(t *testing.T) {
html := buildHTML(sampleResults, "PC-TEST", "2026-07-28 10:00", "", "PC-TEST-Deploy.log.zip")
if strings.Contains(html, "Chyby tohoto behu") {
t.Error("issue-specific line shown when issueURL is empty")
}
if !strings.Contains(html, "xetup-runs/issues?q=PC-TEST") {
t.Error("tracker filter link missing")
}
}
// The footer must only advertise the attachment when one is actually present.
func TestBuildHTMLFooterMatchesAttachment(t *testing.T) {
with := buildHTML(sampleResults, "PC-TEST", "2026-07-28 10:00", "", "PC-TEST-Deploy.log.zip")
if !strings.Contains(with, "v priloze (PC-TEST-Deploy.log.zip)") {
t.Error("footer should name the attachment when one is present")
}
without := buildHTML(sampleResults, "PC-TEST", "2026-07-28 10:00", "", "")
if strings.Contains(without, "v priloze") {
t.Errorf("footer claims an attachment when none is attached:\n%s", without)
}
if !strings.Contains(without, "log lokalne") {
t.Error("footer should still point to the local log copy")
}
}
// zipLog must produce a valid zip whose Deploy.log entry round-trips.
func TestZipLogRoundTrip(t *testing.T) {
content := []byte("line one\nERROR something failed\nline three\n")
name, data := zipLog(writeLog(t, content), "PC-TEST")
if name != "PC-TEST-Deploy.log.zip" {
t.Errorf("unexpected attachment name %q", name)
}
zr, err := zip.NewReader(bytes.NewReader(data), int64(len(data)))
if err != nil {
t.Fatalf("attachment is not a valid zip: %v", err)
}
if len(zr.File) != 1 || zr.File[0].Name != "Deploy.log" {
t.Fatalf("zip should contain exactly Deploy.log, got %v", zr.File)
}
rc, _ := zr.File[0].Open()
got, _ := io.ReadAll(rc)
rc.Close()
if !bytes.Equal(got, content) {
t.Errorf("zip content mismatch: got %q want %q", got, content)
}
}
// zipLog returns no attachment when the log is unreadable (email still sends).
func TestZipLogMissing(t *testing.T) {
name, data := zipLog(filepath.Join(t.TempDir(), "nope.log"), "PC-TEST")
if name != "" || data != nil {
t.Errorf("expected no attachment for missing log, got name=%q len=%d", name, len(data))
}
}
// buildMessage must yield a parseable multipart/mixed message: an HTML part and
// a base64 application/zip attachment that unzips back to the log.
func TestBuildMessageMultipart(t *testing.T) {
logContent := []byte("deploy log body\nERROR boom\n")
_, zipData := zipLog(writeLog(t, logContent), "PC-TEST")
html := buildHTML(sampleResults, "PC-TEST", "2026-07-28 10:00", "https://x/issues/1", "PC-TEST-Deploy.log.zip")
raw := buildMessage("xetup report PC-TEST", html, "PC-TEST-Deploy.log.zip", zipData)
m, err := mail.ReadMessage(bytes.NewReader(raw))
if err != nil {
t.Fatalf("message does not parse: %v", err)
}
mediaType, params, err := mime.ParseMediaType(m.Header.Get("Content-Type"))
if err != nil || mediaType != "multipart/mixed" {
t.Fatalf("expected multipart/mixed, got %q (%v)", mediaType, err)
}
mr := multipart.NewReader(m.Body, params["boundary"])
var sawHTML, sawZip bool
for {
p, err := mr.NextPart()
if err == io.EOF {
break
}
if err != nil {
t.Fatalf("part read error: %v", err)
}
ct := p.Header.Get("Content-Type")
body, _ := io.ReadAll(p)
switch {
case strings.HasPrefix(ct, "text/html"):
sawHTML = true
if !strings.Contains(string(body), "xetup report") {
t.Error("html part missing report content")
}
case strings.HasPrefix(ct, "application/zip"):
sawZip = true
if enc := p.Header.Get("Content-Transfer-Encoding"); enc != "base64" {
t.Errorf("zip part not base64, got %q", enc)
}
// multipart does not auto-decode base64; strip wraps and decode.
clean := strings.NewReplacer("\r", "", "\n", "").Replace(string(body))
dec, err := base64.StdEncoding.DecodeString(clean)
if err != nil {
t.Fatalf("attachment base64 decode failed: %v", err)
}
if _, err := zip.NewReader(bytes.NewReader(dec), int64(len(dec))); err != nil {
t.Errorf("decoded attachment is not a zip: %v", err)
}
}
}
if !sawHTML || !sawZip {
t.Errorf("missing parts: html=%v zip=%v", sawHTML, sawZip)
}
}
// Plain message (no attachment) must be single-part text/html.
func TestBuildMessagePlain(t *testing.T) {
raw := buildMessage("subj", "<html>x</html>", "", nil)
m, err := mail.ReadMessage(bytes.NewReader(raw))
if err != nil {
t.Fatalf("plain message does not parse: %v", err)
}
if ct := m.Header.Get("Content-Type"); !strings.HasPrefix(ct, "text/html") {
t.Errorf("expected text/html, got %q", ct)
}
}