This commit is contained in:
@@ -15,11 +15,6 @@ jobs:
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: "Install webp"
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install libwebp-dev
|
||||
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v4
|
||||
with:
|
||||
|
||||
@@ -2,7 +2,6 @@ name: Release
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: ["main"]
|
||||
tags:
|
||||
- "v*"
|
||||
|
||||
|
||||
326
internal/api/httpinvoce/controller_test.go
Normal file
326
internal/api/httpinvoce/controller_test.go
Normal file
@@ -0,0 +1,326 @@
|
||||
package httpinvoce
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"log/slog"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"git.schreifuchs.ch/lou-taylor/accounting/internal/email"
|
||||
"git.schreifuchs.ch/lou-taylor/accounting/internal/jtype"
|
||||
"git.schreifuchs.ch/lou-taylor/accounting/pkg/invoice"
|
||||
"git.schreifuchs.ch/lou-taylor/accounting/pkg/invoice/issue"
|
||||
"git.schreifuchs.ch/lou-taylor/accounting/pkg/invoice/model"
|
||||
"git.schreifuchs.ch/lou-taylor/accounting/pkg/invoice/report"
|
||||
)
|
||||
|
||||
// Exported versions of the request structs for testing
|
||||
type InvoiceReq struct {
|
||||
Debtor model.Entity `json:"debtor"`
|
||||
Creditor model.Entity `json:"creditor"`
|
||||
DurationThreshold string `json:"durationThreshold"` // Changed to string
|
||||
HourlyRate float64 `json:"hourlyRate"`
|
||||
Repos []string `json:"repositories"`
|
||||
}
|
||||
|
||||
func (i InvoiceReq) GetRepos() (repos []invoice.Repo, err error) {
|
||||
for i, repo := range i.Repos {
|
||||
parts := strings.Split(repo, "/")
|
||||
if len(parts) != 2 {
|
||||
err = fmt.Errorf("cannot read body: repo with index %d does not split into 2 parts", i)
|
||||
return
|
||||
}
|
||||
repos = append(repos, invoice.Repo{
|
||||
Owner: parts[0],
|
||||
Repo: parts[1],
|
||||
})
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
type SendReq struct {
|
||||
To []string `json:"to"`
|
||||
Cc []string `json:"cc"`
|
||||
Bcc []string `json:"bcc"`
|
||||
Subject string `json:"subject"`
|
||||
Body string `json:"body"`
|
||||
Invoice InvoiceReq `json:"invoice"`
|
||||
}
|
||||
|
||||
func (s SendReq) ToEMail() email.Mail {
|
||||
return email.Mail{
|
||||
To: s.To,
|
||||
Cc: s.Cc,
|
||||
Bcc: s.Bcc,
|
||||
Subject: s.Subject,
|
||||
Body: s.Body,
|
||||
}
|
||||
}
|
||||
|
||||
// MockInvoiceService mocks the invoice.Service interface
|
||||
type MockInvoiceService struct {
|
||||
GenerateFunc func(creditor, debtor model.Entity, durationThreshold time.Duration, hourlyRate float64, repos []invoice.Repo) (io.ReadCloser, *report.Report, error)
|
||||
}
|
||||
|
||||
func (m *MockInvoiceService) Generate(creditor, debtor model.Entity, durationThreshold time.Duration, hourlyRate float64, repos []invoice.Repo) (io.ReadCloser, *report.Report, error) {
|
||||
if m.GenerateFunc != nil {
|
||||
return m.GenerateFunc(creditor, debtor, durationThreshold, hourlyRate, repos)
|
||||
}
|
||||
return nil, &report.Report{}, nil
|
||||
}
|
||||
|
||||
// MockEmailService mocks the email.Service interface
|
||||
type MockEmailService struct {
|
||||
SendFunc func(mail email.Mail) error
|
||||
}
|
||||
|
||||
func (m *MockEmailService) Send(mail email.Mail) error {
|
||||
if m.SendFunc != nil {
|
||||
return m.SendFunc(mail)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func TestCreateInvoice(t *testing.T) {
|
||||
// Create a dummy logger for the service
|
||||
dummyLogger := slog.New(slog.NewTextHandler(io.Discard, nil))
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
requestBody InvoiceReq
|
||||
mockGenerate func(creditor, debtor model.Entity, durationThreshold time.Duration, hourlyRate float64, repos []invoice.Repo) (io.ReadCloser, *report.Report, error)
|
||||
expectedStatus int
|
||||
expectedBody string // For error messages or specific content
|
||||
}{
|
||||
{
|
||||
name: "successful invoice creation",
|
||||
requestBody: InvoiceReq{
|
||||
Creditor: model.Entity{Name: "Creditor"},
|
||||
Debtor: model.Entity{Name: "Debtor"},
|
||||
DurationThreshold: "1h", // Changed to string
|
||||
HourlyRate: 100,
|
||||
Repos: []string{"repo1"},
|
||||
},
|
||||
mockGenerate: func(creditor, debtor model.Entity, durationThreshold time.Duration, hourlyRate float64, repos []invoice.Repo) (io.ReadCloser, *report.Report, error) {
|
||||
pdfContent := "mock PDF content"
|
||||
// Create a report with a positive total duration
|
||||
mockReport := report.New([]issue.Issue{{Duration: time.Hour}}, model.Entity{}, model.Entity{}, 0)
|
||||
return io.NopCloser(bytes.NewReader([]byte(pdfContent))), mockReport, nil
|
||||
},
|
||||
expectedStatus: http.StatusOK,
|
||||
expectedBody: "mock PDF content",
|
||||
},
|
||||
{
|
||||
name: "invalid request body",
|
||||
requestBody: InvoiceReq{ // Malformed request body
|
||||
Creditor: model.Entity{Name: "Creditor"},
|
||||
Debtor: model.Entity{Name: "Debtor"},
|
||||
DurationThreshold: "invalid", // Changed to string
|
||||
HourlyRate: 100,
|
||||
Repos: []string{"repo1"},
|
||||
},
|
||||
mockGenerate: nil, // Not called for invalid body
|
||||
expectedStatus: http.StatusBadRequest,
|
||||
expectedBody: "cannot read body", // Partial match for error message
|
||||
},
|
||||
{
|
||||
name: "invoice generation error",
|
||||
requestBody: InvoiceReq{
|
||||
Creditor: model.Entity{Name: "Creditor"},
|
||||
Debtor: model.Entity{Name: "Debtor"},
|
||||
DurationThreshold: "1h", // Changed to string
|
||||
HourlyRate: 100,
|
||||
Repos: []string{"repo1"},
|
||||
},
|
||||
mockGenerate: func(creditor, debtor model.Entity, durationThreshold time.Duration, hourlyRate float64, repos []invoice.Repo) (io.ReadCloser, *report.Report, error) {
|
||||
return nil, nil, errors.New("failed to generate invoice")
|
||||
},
|
||||
expectedStatus: http.StatusInternalServerError,
|
||||
expectedBody: "internal server error",
|
||||
},
|
||||
{
|
||||
name: "no suitable issues to be billed",
|
||||
requestBody: InvoiceReq{
|
||||
Creditor: model.Entity{Name: "Creditor"},
|
||||
Debtor: model.Entity{Name: "Debtor"},
|
||||
DurationThreshold: "1h", // Changed to string
|
||||
HourlyRate: 100,
|
||||
Repos: []string{"repo1"},
|
||||
},
|
||||
mockGenerate: func(creditor, debtor model.Entity, durationThreshold time.Duration, hourlyRate float64, repos []invoice.Repo) (io.ReadCloser, *report.Report, error) {
|
||||
// Create a report with zero total duration
|
||||
mockReport := report.New([]issue.Issue{}, model.Entity{}, model.Entity{}, 0)
|
||||
return io.NopCloser(bytes.NewReader([]byte("mock PDF content"))), mockReport, nil
|
||||
},
|
||||
expectedStatus: http.StatusNotFound,
|
||||
expectedBody: "no suitable issues to be billed",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
mockInvoiceService := &MockInvoiceService{
|
||||
GenerateFunc: tt.mockGenerate,
|
||||
}
|
||||
service := Service{invoice: mockInvoiceService, log: dummyLogger} // Pass the dummy logger
|
||||
|
||||
reqBody, _ := json.Marshal(tt.requestBody)
|
||||
fmt.Println("Request Body:", string(reqBody)) // Debug print
|
||||
req := httptest.NewRequest(http.MethodPost, "/invoice", bytes.NewBuffer(reqBody))
|
||||
rr := httptest.NewRecorder()
|
||||
|
||||
service.createInvoice(rr, req)
|
||||
|
||||
if rr.Code != tt.expectedStatus {
|
||||
t.Errorf("expected status %d, got %d", tt.expectedStatus, rr.Code)
|
||||
}
|
||||
|
||||
body := rr.Body.String()
|
||||
if tt.expectedBody != "" && !bytes.Contains([]byte(body), []byte(tt.expectedBody)) {
|
||||
t.Errorf("expected body to contain %q, got %q", tt.expectedBody, body)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestSendInvoice(t *testing.T) {
|
||||
// Create a dummy logger for the service
|
||||
dummyLogger := slog.New(slog.NewTextHandler(io.Discard, nil))
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
requestBody SendReq
|
||||
mockGenerate func(creditor, debtor model.Entity, durationThreshold time.Duration, hourlyRate float64, repos []invoice.Repo) (io.ReadCloser, *report.Report, error)
|
||||
mockSend func(mail email.Mail) error
|
||||
expectedStatus int
|
||||
expectedBody string
|
||||
}{
|
||||
{
|
||||
name: "successful invoice send",
|
||||
requestBody: SendReq{
|
||||
To: []string{"test@example.com"},
|
||||
Subject: "Test Invoice",
|
||||
Body: "Here is your invoice.",
|
||||
Invoice: InvoiceReq{
|
||||
Creditor: model.Entity{Name: "Creditor"},
|
||||
Debtor: model.Entity{Name: "Debtor"},
|
||||
DurationThreshold: "1h", // Changed to string
|
||||
HourlyRate: 100,
|
||||
Repos: []string{"repo1"},
|
||||
},
|
||||
},
|
||||
mockGenerate: func(creditor, debtor model.Entity, durationThreshold time.Duration, hourlyRate float64, repos []invoice.Repo) (io.ReadCloser, *report.Report, error) {
|
||||
pdfContent := "mock PDF content for send"
|
||||
mockReport := report.New([]issue.Issue{{Duration: time.Hour}}, model.Entity{}, model.Entity{}, 0)
|
||||
return io.NopCloser(bytes.NewReader([]byte(pdfContent))), mockReport, nil
|
||||
},
|
||||
mockSend: func(mail email.Mail) error {
|
||||
return nil // Simulate successful email send
|
||||
},
|
||||
expectedStatus: http.StatusOK,
|
||||
expectedBody: "mock PDF content for send",
|
||||
},
|
||||
{
|
||||
name: "invoice generation error during send",
|
||||
requestBody: SendReq{
|
||||
To: []string{"test@example.com"},
|
||||
Subject: "Test Invoice",
|
||||
Body: "Here is your invoice.",
|
||||
Invoice: InvoiceReq{
|
||||
Creditor: model.Entity{Name: "Creditor"},
|
||||
Debtor: model.Entity{Name: "Debtor"},
|
||||
DurationThreshold: "1h", // Changed to string
|
||||
HourlyRate: 100,
|
||||
Repos: []string{"repo1"},
|
||||
},
|
||||
},
|
||||
mockGenerate: func(creditor, debtor model.Entity, durationThreshold time.Duration, hourlyRate float64, repos []invoice.Repo) (io.ReadCloser, *report.Report, error) {
|
||||
return nil, nil, errors.New("failed to generate invoice for send")
|
||||
},
|
||||
mockSend: nil, // Not called
|
||||
expectedStatus: http.StatusInternalServerError,
|
||||
expectedBody: "error while processing invoice:",
|
||||
},
|
||||
{
|
||||
name: "email send error",
|
||||
requestBody: SendReq{
|
||||
To: []string{"test@example.com"},
|
||||
Subject: "Test Invoice",
|
||||
Body: "Here is your invoice.",
|
||||
Invoice: InvoiceReq{
|
||||
Creditor: model.Entity{Name: "Creditor"},
|
||||
Debtor: model.Entity{Name: "Debtor"},
|
||||
DurationThreshold: "1h", // Changed to string
|
||||
HourlyRate: 100,
|
||||
Repos: []string{"repo1"},
|
||||
},
|
||||
},
|
||||
mockGenerate: func(creditor, debtor model.Entity, durationThreshold time.Duration, hourlyRate float64, repos []invoice.Repo) (io.ReadCloser, *report.Report, error) {
|
||||
pdfContent := "mock PDF content for send"
|
||||
mockReport := report.New([]issue.Issue{{Duration: time.Hour}}, model.Entity{}, model.Entity{}, 0)
|
||||
return io.NopCloser(bytes.NewReader([]byte(pdfContent))), mockReport, nil
|
||||
},
|
||||
mockSend: func(mail email.Mail) error {
|
||||
return errors.New("failed to send email")
|
||||
},
|
||||
expectedStatus: http.StatusInternalServerError,
|
||||
expectedBody: "error while sending mail:",
|
||||
},
|
||||
{
|
||||
name: "no suitable issues to be billed during send",
|
||||
requestBody: SendReq{
|
||||
To: []string{"test@example.com"},
|
||||
Subject: "Test Invoice",
|
||||
Body: "Here is your invoice.",
|
||||
Invoice: InvoiceReq{
|
||||
Creditor: model.Entity{Name: "Creditor"},
|
||||
Debtor: model.Entity{Name: "Debtor"},
|
||||
DurationThreshold: "1h", // Changed to string
|
||||
HourlyRate: 100,
|
||||
Repos: []string{"repo1"},
|
||||
},
|
||||
},
|
||||
mockGenerate: func(creditor, debtor model.Entity, durationThreshold time.Duration, hourlyRate float64, repos []invoice.Repo) (io.ReadCloser, *report.Report, error) {
|
||||
mockReport := report.New([]issue.Issue{}, model.Entity{}, model.Entity{}, 0)
|
||||
return io.NopCloser(bytes.NewReader([]byte("mock PDF content"))), mockReport, nil
|
||||
},
|
||||
expectedStatus: http.StatusNotFound,
|
||||
expectedBody: "no suitable issues to be billed",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
mockInvoiceService := &MockInvoiceService{
|
||||
GenerateFunc: tt.mockGenerate,
|
||||
}
|
||||
mockEmailService := &MockEmailService{
|
||||
SendFunc: tt.mockSend,
|
||||
}
|
||||
service := Service{invoice: mockInvoiceService, mail: mockEmailService, log: dummyLogger} // Pass the dummy logger
|
||||
|
||||
reqBody, _ := json.Marshal(tt.requestBody)
|
||||
fmt.Println("Request Body:", string(reqBody)) // Debug print
|
||||
req := httptest.NewRequest(http.MethodPost, "/invoice/send", bytes.NewBuffer(reqBody))
|
||||
rr := httptest.NewRecorder()
|
||||
|
||||
service.sendInvoice(rr, req)
|
||||
|
||||
if rr.Code != tt.expectedStatus {
|
||||
t.Errorf("expected status %d, got %d", tt.expectedStatus, rr.Code)
|
||||
}
|
||||
|
||||
body := rr.Body.String()
|
||||
if tt.expectedBody != "" && !bytes.Contains([]byte(body), []byte(tt.expectedBody)) {
|
||||
t.Errorf("expected body to contain %q, got %q", tt.expectedBody, body)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user