feat: tests

This commit is contained in:
2025-11-09 16:39:29 +01:00
parent e57185b057
commit 7913e6b900
4 changed files with 189 additions and 1 deletions

View File

@@ -25,6 +25,22 @@ Alternatively, you can use Docker for a containerized environment:
docker-compose up --build docker-compose up --build
``` ```
## Writing Tests
All tests are written using the go standard library and are table driven.
In this application no libraries like testify are used. Mocks are written in
the test file using this schema:
```go
type Mock struct {
Foo func (string) string
}
func (m Mock) Foo(in string) string {
return m.Foo(in)
}
```
## Running Tests ## Running Tests
To run all tests for the project, use the following command: To run all tests for the project, use the following command:

6
go.mod
View File

@@ -6,22 +6,26 @@ require (
code.gitea.io/sdk/gitea v0.21.0 code.gitea.io/sdk/gitea v0.21.0
github.com/caarlos0/env/v10 v10.0.0 github.com/caarlos0/env/v10 v10.0.0
github.com/gomarkdown/markdown v0.0.0-20250810172220-2e2c11897d1a github.com/gomarkdown/markdown v0.0.0-20250810172220-2e2c11897d1a
github.com/itzg/go-flagsfiller v1.16.0
github.com/jedib0t/go-pretty/v6 v6.6.8 github.com/jedib0t/go-pretty/v6 v6.6.8
github.com/jordan-wright/email v4.0.1-0.20210109023952-943e75fe5223+incompatible github.com/jordan-wright/email v4.0.1-0.20210109023952-943e75fe5223+incompatible
github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e
github.com/starwalkn/gotenberg-go-client/v8 v8.11.0 github.com/starwalkn/gotenberg-go-client/v8 v8.11.0
github.com/itzg/go-flagsfiller v1.16.0
) )
require ( require (
github.com/42wim/httpsig v1.2.3 // indirect github.com/42wim/httpsig v1.2.3 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/davidmz/go-pageant v1.0.2 // indirect github.com/davidmz/go-pageant v1.0.2 // indirect
github.com/go-fed/httpsig v1.1.0 // indirect github.com/go-fed/httpsig v1.1.0 // indirect
github.com/hashicorp/go-version v1.7.0 // indirect github.com/hashicorp/go-version v1.7.0 // indirect
github.com/iancoleman/strcase v0.3.0 // indirect github.com/iancoleman/strcase v0.3.0 // indirect
github.com/mattn/go-runewidth v0.0.16 // indirect github.com/mattn/go-runewidth v0.0.16 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/rivo/uniseg v0.4.7 // indirect github.com/rivo/uniseg v0.4.7 // indirect
github.com/stretchr/objx v0.5.2 // indirect
golang.org/x/crypto v0.41.0 // indirect golang.org/x/crypto v0.41.0 // indirect
golang.org/x/sys v0.35.0 // indirect golang.org/x/sys v0.35.0 // indirect
golang.org/x/text v0.28.0 // indirect golang.org/x/text v0.28.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
) )

6
go.sum
View File

@@ -31,8 +31,12 @@ github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e h1:MRM5ITcdelLK2j1vwZ3Je0FKVCfqOLp5zO6trqMLYs0= github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e h1:MRM5ITcdelLK2j1vwZ3Je0FKVCfqOLp5zO6trqMLYs0=
github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e/go.mod h1:XV66xRDqSt+GTGFMVlhk3ULuV0y9ZmzeVGR4mloJI3M= github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e/go.mod h1:XV66xRDqSt+GTGFMVlhk3ULuV0y9ZmzeVGR4mloJI3M=
github.com/spf13/pflag v1.0.6 h1:jFzHGLGAlb3ruxLB8MhbI6A8+AQX/2eW4qeyNZXNp2o=
github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/starwalkn/gotenberg-go-client/v8 v8.11.0 h1:9hKCcoWegZ0yrZ0tEl5gKlMyH2YraYO0R7zDyL0ZL/U= github.com/starwalkn/gotenberg-go-client/v8 v8.11.0 h1:9hKCcoWegZ0yrZ0tEl5gKlMyH2YraYO0R7zDyL0ZL/U=
github.com/starwalkn/gotenberg-go-client/v8 v8.11.0/go.mod h1:5q9nAJ3/lub4hSCT6QYl8cOjnfQ/B+spEzC4TAPVXd8= github.com/starwalkn/gotenberg-go-client/v8 v8.11.0/go.mod h1:5q9nAJ3/lub4hSCT6QYl8cOjnfQ/B+spEzC4TAPVXd8=
github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY=
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
@@ -55,5 +59,7 @@ golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.28.0 h1:rhazDwis8INMIwQ4tpjLDzUhx6RlXqZNPEM0huQojng= golang.org/x/text v0.28.0 h1:rhazDwis8INMIwQ4tpjLDzUhx6RlXqZNPEM0huQojng=
golang.org/x/text v0.28.0/go.mod h1:U8nCwOR8jO/marOQ0QbDiOngZVEBB7MAiitBuMjXiNU= golang.org/x/text v0.28.0/go.mod h1:U8nCwOR8jO/marOQ0QbDiOngZVEBB7MAiitBuMjXiNU=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

162
pkg/invoice/invoice_test.go Normal file
View File

@@ -0,0 +1,162 @@
package invoice
import (
"bytes"
"errors"
"io"
"log/slog"
"strings"
"testing"
"time"
"code.gitea.io/sdk/gitea"
"git.schreifuchs.ch/lou-taylor/accounting/pkg/invoice/model"
)
// MockGiteaClient is a mock implementation of the GiteaClient interface.
type MockGiteaClient struct {
ListRepoIssuesFunc func(owner, repo string, opt gitea.ListIssueOption) ([]*gitea.Issue, *gitea.Response, error)
}
func (m *MockGiteaClient) ListRepoIssues(owner, repo string, opt gitea.ListIssueOption) ([]*gitea.Issue, *gitea.Response, error) {
if m.ListRepoIssuesFunc != nil {
return m.ListRepoIssuesFunc(owner, repo, opt)
}
return nil, nil, errors.New("ListRepoIssuesFunc not implemented")
}
// MockPdfGenerator is a mock implementation of the PdfGenerator interface.
type MockPdfGenerator struct {
HtmlToPdfFunc func(html string) (io.ReadCloser, error)
}
func (m *MockPdfGenerator) HtmlToPdf(html string) (io.ReadCloser, error) {
if m.HtmlToPdfFunc != nil {
return m.HtmlToPdfFunc(html)
}
return nil, errors.New("HtmlToPdfFunc not implemented")
}
func TestGenerate(t *testing.T) {
creditor := model.Entity{
Name: "creditor",
}
deptor := model.Entity{
Name: "deptor",
}
rate := 100.0
repos := []Repo{
{Owner: "owner", Repo: "repo"},
}
testCases := []struct {
name string
setupMocks func(*MockGiteaClient, *MockPdfGenerator)
config *Options
expectedError string
}{
{
name: "successful generation",
setupMocks: func(g *MockGiteaClient, p *MockPdfGenerator) {
g.ListRepoIssuesFunc = func(owner, repo string, opt gitea.ListIssueOption) ([]*gitea.Issue, *gitea.Response, error) {
return []*gitea.Issue{
{ID: 1, Title: "Test Issue", Body: "```info\nduration: 1h\n```"},
}, &gitea.Response{}, nil
}
p.HtmlToPdfFunc = func(html string) (io.ReadCloser, error) {
return io.NopCloser(bytes.NewReader([]byte("pdf"))), nil
}
},
config: &DefaultOptions,
expectedError: "",
},
{
name: "gitea error",
setupMocks: func(g *MockGiteaClient, p *MockPdfGenerator) {
g.ListRepoIssuesFunc = func(owner, repo string, opt gitea.ListIssueOption) ([]*gitea.Issue, *gitea.Response, error) {
return nil, nil, errors.New("gitea error")
}
},
config: &DefaultOptions,
expectedError: "gitea error",
},
{
name: "pdf error",
setupMocks: func(g *MockGiteaClient, p *MockPdfGenerator) {
g.ListRepoIssuesFunc = func(owner, repo string, opt gitea.ListIssueOption) ([]*gitea.Issue, *gitea.Response, error) {
return []*gitea.Issue{
{ID: 1, Title: "Test Issue", Body: "```info\nduration: 1h\n```"},
}, &gitea.Response{}, nil
}
p.HtmlToPdfFunc = func(html string) (io.ReadCloser, error) {
return nil, errors.New("pdf error")
}
},
config: &DefaultOptions,
expectedError: "pdf error",
},
{
name: "no issues",
setupMocks: func(g *MockGiteaClient, p *MockPdfGenerator) {
g.ListRepoIssuesFunc = func(owner, repo string, opt gitea.ListIssueOption) ([]*gitea.Issue, *gitea.Response, error) {
return []*gitea.Issue{}, &gitea.Response{}, nil
}
p.HtmlToPdfFunc = func(html string) (io.ReadCloser, error) {
return io.NopCloser(bytes.NewReader([]byte("pdf"))), nil
}
},
config: &DefaultOptions,
expectedError: "",
},
{
name: "custom template",
setupMocks: func(g *MockGiteaClient, p *MockPdfGenerator) {
g.ListRepoIssuesFunc = func(owner, repo string, opt gitea.ListIssueOption) ([]*gitea.Issue, *gitea.Response, error) {
return []*gitea.Issue{
{ID: 1, Title: "Test Issue", Body: "```info\nduration: 1h\n```"},
}, &gitea.Response{}, nil
}
p.HtmlToPdfFunc = func(html string) (io.ReadCloser, error) {
return io.NopCloser(bytes.NewReader([]byte("pdf"))), nil
}
},
config: &Options{
Mindur: time.Minute * 15,
Since: time.Now().AddDate(0, -1, 0),
Before: time.Now(),
IssueState: gitea.StateClosed,
IssueFilter: func(i *gitea.Issue) bool { return true },
CustomTemplate: "custom template",
},
expectedError: "",
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
giteaClient := new(MockGiteaClient)
pdfGenerator := new(MockPdfGenerator)
tc.setupMocks(giteaClient, pdfGenerator)
service := New(slog.Default(), giteaClient, pdfGenerator)
doc, _, err := service.Generate(creditor, &deptor, rate, repos, tc.config)
if tc.expectedError != "" {
if err == nil {
t.Fatalf("expected an error, but got none")
}
if !strings.Contains(err.Error(), tc.expectedError) {
t.Errorf("expected error to contain '%s', but got '%s'", tc.expectedError, err.Error())
}
} else {
if err != nil {
t.Fatalf("expected no error, but got: %v", err)
}
if doc == nil {
t.Fatal("expected a document, but got nil")
}
}
})
}
}