Skip to content

Commit 7d6c97a

Browse files
Add unit tests for utility functions (prequel-dev#53)
* Add additional tests for config, runbook, and utils * chore: Updated coverage badge. * Add additional unit tests * chore: Updated coverage badge. --------- Co-authored-by: GitHub Action <action@github.com>
1 parent b761cb4 commit 7d6c97a

File tree

6 files changed

+500
-1
lines changed

6 files changed

+500
-1
lines changed

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
# preq
2-
![Coverage](https://img.shields.io/badge/Coverage-19.6%25-red)
2+
![Coverage](https://img.shields.io/badge/Coverage-29.7%25-red)
33
[![Unit Tests](https://github.com/prequel-dev/cre/actions/workflows/build.yml/badge.svg)](https://github.com/prequel-dev/cre/actions/workflows/build.yml)
44
[![Unit Tests](https://github.com/prequel-dev/preq/actions/workflows/build.yml/badge.svg)](https://github.com/prequel-dev/preq/actions/workflows/build.yml)
55
[![Unit Tests](https://github.com/prequel-dev/prequel-compiler/actions/workflows/build.yml/badge.svg)](https://github.com/prequel-dev/prequel-compiler/actions/workflows/build.yml)

internal/pkg/config/config_test.go

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
package config_test
2+
3+
import (
4+
"os"
5+
"path/filepath"
6+
"strings"
7+
"testing"
8+
"time"
9+
10+
"github.com/prequel-dev/preq/internal/pkg/config"
11+
)
12+
13+
func TestMarshal(t *testing.T) {
14+
out := config.Marshal()
15+
if !strings.Contains(out, "timestamps:") {
16+
t.Fatalf("expected timestamps in output")
17+
}
18+
19+
out = config.Marshal(config.WithWindow(2 * time.Second))
20+
if !strings.Contains(out, "window: 2s") {
21+
t.Fatalf("expected window option in output")
22+
}
23+
}
24+
25+
func TestLoadConfig(t *testing.T) {
26+
dir := t.TempDir()
27+
cfg, err := config.LoadConfig(dir, "cfg.yaml", config.WithWindow(3*time.Second))
28+
if err != nil {
29+
t.Fatalf("LoadConfig error: %v", err)
30+
}
31+
if cfg.Window != 3*time.Second {
32+
t.Fatalf("expected window 3s got %v", cfg.Window)
33+
}
34+
if len(cfg.TimestampRegexes) == 0 {
35+
t.Fatalf("expected default timestamp regexes")
36+
}
37+
if _, err := os.Stat(filepath.Join(dir, "cfg.yaml")); err != nil {
38+
t.Fatalf("expected config file written: %v", err)
39+
}
40+
}
41+
42+
func TestLoadConfigFromBytes(t *testing.T) {
43+
data := "timestamps: []\nwindow: 1s\n"
44+
cfg, err := config.LoadConfigFromBytes(data)
45+
if err != nil {
46+
t.Fatalf("LoadConfigFromBytes: %v", err)
47+
}
48+
if cfg.Window != time.Second {
49+
t.Fatalf("expected 1s window, got %v", cfg.Window)
50+
}
51+
}
52+
53+
func TestWriteDefaultConfigAndResolveOpts(t *testing.T) {
54+
dir := t.TempDir()
55+
path := filepath.Join(dir, "cfg.yaml")
56+
if err := config.WriteDefaultConfig(path, config.WithWindow(2*time.Second)); err != nil {
57+
t.Fatalf("WriteDefaultConfig: %v", err)
58+
}
59+
data, err := os.ReadFile(path)
60+
if err != nil {
61+
t.Fatalf("read file: %v", err)
62+
}
63+
if !strings.Contains(string(data), "window: 2s") {
64+
t.Fatalf("window option missing")
65+
}
66+
67+
cfg, err := config.LoadConfig(dir, "cfg.yaml")
68+
if err != nil {
69+
t.Fatalf("LoadConfig: %v", err)
70+
}
71+
if len(cfg.ResolveOpts()) == 0 {
72+
t.Fatalf("expected resolve options")
73+
}
74+
}
Lines changed: 194 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,194 @@
1+
package runbook
2+
3+
import (
4+
"bytes"
5+
"context"
6+
"github.com/prequel-dev/preq/internal/pkg/ux"
7+
"io"
8+
"net/http"
9+
"net/http/httptest"
10+
"os"
11+
"path/filepath"
12+
"regexp"
13+
"testing"
14+
"text/template"
15+
)
16+
17+
type stubAction struct{ called bool }
18+
19+
func (s *stubAction) Execute(ctx context.Context, m map[string]any) error {
20+
s.called = true
21+
return nil
22+
}
23+
24+
func TestFilteredAction(t *testing.T) {
25+
a := &stubAction{}
26+
f := &filteredAction{pattern: regexp.MustCompile("CRE-1"), inner: a}
27+
28+
// no match
29+
if err := f.Execute(context.Background(), map[string]any{"cre": map[string]any{"id": "CRE-2"}}); err != nil {
30+
t.Fatalf("unexpected error: %v", err)
31+
}
32+
if a.called {
33+
t.Fatalf("action should not run")
34+
}
35+
36+
// match
37+
if err := f.Execute(context.Background(), map[string]any{"cre": map[string]any{"id": "CRE-1"}}); err != nil {
38+
t.Fatalf("unexpected error: %v", err)
39+
}
40+
if !a.called {
41+
t.Fatalf("action should run")
42+
}
43+
}
44+
45+
func TestFuncMapAndExecuteTemplate(t *testing.T) {
46+
data := map[string]any{"cre": map[string]any{"ID": "CRE-9"}, "desc": "- message"}
47+
funcs := funcMap()
48+
tmpl, err := template.New("t").Funcs(funcs).Parse("{{field .cre \"ID\"}} {{stripdash .desc}}")
49+
if err != nil {
50+
t.Fatalf("template parse: %v", err)
51+
}
52+
var out string
53+
if err := executeTemplate(&out, tmpl, data); err != nil {
54+
t.Fatalf("executeTemplate: %v", err)
55+
}
56+
if out != "CRE-9 message" {
57+
t.Fatalf("got %q", out)
58+
}
59+
}
60+
61+
func TestExtractCreId(t *testing.T) {
62+
ev := map[string]any{"cre": map[string]any{"id": "A"}}
63+
if id := extractCreId(ev); id != "A" {
64+
t.Fatalf("map: expected A got %s", id)
65+
}
66+
type cre struct{ ID string }
67+
ev["cre"] = &cre{ID: "B"}
68+
if id := extractCreId(ev); id != "B" {
69+
t.Fatalf("struct: expected B got %s", id)
70+
}
71+
delete(ev, "cre")
72+
ev["id"] = "C"
73+
if id := extractCreId(ev); id != "C" {
74+
t.Fatalf("top-level: expected C got %s", id)
75+
}
76+
}
77+
78+
func TestNewExecAction(t *testing.T) {
79+
_, err := newExecAction(execConfig{})
80+
if err == nil {
81+
t.Fatalf("expected error for missing path")
82+
}
83+
script := filepath.Join(t.TempDir(), "script.sh")
84+
os.WriteFile(script, []byte("#!/bin/sh\ncat >/dev/null"), 0755)
85+
a, err := newExecAction(execConfig{Path: script})
86+
if err != nil {
87+
t.Fatalf("newExecAction: %v", err)
88+
}
89+
if err := a.Execute(context.Background(), map[string]any{"id": "x"}); err != nil {
90+
t.Fatalf("execute: %v", err)
91+
}
92+
}
93+
94+
func TestNewSlackAction(t *testing.T) {
95+
_, err := newSlackAction(slackConfig{})
96+
if err == nil {
97+
t.Fatalf("expected error for missing fields")
98+
}
99+
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
100+
body, _ := io.ReadAll(r.Body)
101+
if !bytes.Contains(body, []byte("CRE-5")) {
102+
t.Errorf("missing id")
103+
}
104+
w.WriteHeader(200)
105+
}))
106+
defer srv.Close()
107+
a, err := newSlackAction(slackConfig{WebhookURL: srv.URL, MessageTemplate: "{{field .cre \"ID\"}}"})
108+
if err != nil {
109+
t.Fatalf("newSlackAction: %v", err)
110+
}
111+
err = a.Execute(context.Background(), map[string]any{"cre": map[string]any{"ID": "CRE-5"}})
112+
if err != nil {
113+
t.Fatalf("execute slack: %v", err)
114+
}
115+
}
116+
117+
func TestBuildActions(t *testing.T) {
118+
script := filepath.Join(t.TempDir(), "run.sh")
119+
os.WriteFile(script, []byte("#!/bin/sh\nexit 0"), 0755)
120+
cfg := "actions:\n- type: exec\n exec:\n path: " + script + "\n"
121+
path := filepath.Join(t.TempDir(), "cfg.yaml")
122+
os.WriteFile(path, []byte(cfg), 0644)
123+
acts, err := buildActions(path)
124+
if err != nil {
125+
t.Fatalf("buildActions: %v", err)
126+
}
127+
if len(acts) != 1 {
128+
t.Fatalf("expected 1 action got %d", len(acts))
129+
}
130+
}
131+
132+
func TestNewJiraActionAndAdfParagraph(t *testing.T) {
133+
_, err := newJiraAction(jiraConfig{})
134+
if err == nil {
135+
t.Fatalf("expected error for missing fields")
136+
}
137+
os.Setenv("JIRA_TOKEN", "tok")
138+
defer os.Unsetenv("JIRA_TOKEN")
139+
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
140+
body, _ := io.ReadAll(r.Body)
141+
if !bytes.Contains(body, []byte("CRE-7")) {
142+
t.Errorf("missing id")
143+
}
144+
w.WriteHeader(200)
145+
}))
146+
defer srv.Close()
147+
cfg := jiraConfig{
148+
WebhookURL: srv.URL,
149+
SecretEnv: "JIRA_TOKEN",
150+
SummaryTemplate: "{{field .cre \"ID\"}}",
151+
DescriptionTemplate: "d",
152+
ProjectKey: "PR",
153+
}
154+
a, err := newJiraAction(cfg)
155+
if err != nil {
156+
t.Fatalf("newJiraAction: %v", err)
157+
}
158+
para := adfParagraph("x")
159+
if para["type"] != "doc" {
160+
t.Fatalf("unexpected adf")
161+
}
162+
if err := a.Execute(context.Background(), map[string]any{"cre": map[string]any{"ID": "CRE-7"}}); err != nil {
163+
t.Fatalf("execute: %v", err)
164+
}
165+
}
166+
167+
func TestNewLinearAction(t *testing.T) {
168+
_, err := newLinearAction(linearConfig{})
169+
if err == nil {
170+
t.Fatalf("expected error for missing fields")
171+
}
172+
os.Setenv("LIN_TOKEN", "tok")
173+
defer os.Unsetenv("LIN_TOKEN")
174+
cfg := linearConfig{TeamID: "T", SecretEnv: "LIN_TOKEN", TitleTemplate: "{{.}}", DescriptionTemplate: "d"}
175+
a, err := newLinearAction(cfg)
176+
if err != nil {
177+
t.Fatalf("newLinearAction: %v", err)
178+
}
179+
if a == nil {
180+
t.Fatalf("expected action")
181+
}
182+
}
183+
184+
func TestRunbook(t *testing.T) {
185+
script := filepath.Join(t.TempDir(), "run.sh")
186+
os.WriteFile(script, []byte("#!/bin/sh\nexit 0"), 0755)
187+
cfg := "actions:\n- type: exec\n exec:\n path: " + script + "\n"
188+
path := filepath.Join(t.TempDir(), "cfg.yaml")
189+
os.WriteFile(path, []byte(cfg), 0644)
190+
report := ux.ReportDocT{{"cre": map[string]any{"ID": "CRE"}}}
191+
if err := Runbook(context.Background(), path, report); err != nil {
192+
t.Fatalf("Runbook: %v", err)
193+
}
194+
}

internal/pkg/timez/timez_test.go

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
package timez_test
2+
3+
import (
4+
"testing"
5+
"time"
6+
7+
"github.com/prequel-dev/preq/internal/pkg/timez"
8+
)
9+
10+
func TestGetTimestampFormat(t *testing.T) {
11+
cb, err := timez.GetTimestampFormat(timez.FmtRfc3339)
12+
if err != nil {
13+
t.Fatalf("unexpected error: %v", err)
14+
}
15+
ts, err := cb([]byte("2025-01-02T03:04:05Z"))
16+
if err != nil {
17+
t.Fatalf("parse failed: %v", err)
18+
}
19+
want := time.Date(2025, 1, 2, 3, 4, 5, 0, time.UTC).UnixNano()
20+
if ts != want {
21+
t.Fatalf("expected %d got %d", want, ts)
22+
}
23+
}
24+
25+
func TestEpochParserAndAny(t *testing.T) {
26+
cb, _ := timez.GetTimestampFormat(timez.FmtEpochMillis)
27+
ts, err := cb([]byte("42"))
28+
if err != nil || ts != 42*int64(time.Millisecond) {
29+
t.Fatalf("epoch millis failed")
30+
}
31+
32+
cb, _ = timez.GetTimestampFormat(timez.FmtEpochAny)
33+
ts, err = cb([]byte("1000"))
34+
if err != nil || ts != 1000*int64(time.Second) {
35+
t.Fatalf("epoch any failed")
36+
}
37+
}
38+
39+
func TestTryTimestampFormat(t *testing.T) {
40+
line := "2025-06-06T12:00:00Z first line\nsecond" // newline ensures only first line used
41+
factory, ts, err := timez.TryTimestampFormat(`^(\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}Z)`, timez.FmtRfc3339, []byte(line), 1)
42+
if err != nil {
43+
t.Fatalf("unexpected error: %v", err)
44+
}
45+
if factory == nil {
46+
t.Fatal("expected factory")
47+
}
48+
want := time.Date(2025, 6, 6, 12, 0, 0, 0, time.UTC).UnixNano()
49+
if ts != want {
50+
t.Fatalf("timestamp mismatch")
51+
}
52+
}

0 commit comments

Comments
 (0)