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) 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", "") 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") } } // 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") 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", "x", "", 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) } }