All checks were successful
release / build-and-release (push) Successful in 35s
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>
170 lines
5.6 KiB
Go
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)
|
|
}
|
|
}
|