Skip to content

Commit e1d841c

Browse files
committed
chore: configure SonarQube integration, update dev environment scripts, and refactor tests for readability.
1 parent da073da commit e1d841c

17 files changed

Lines changed: 249 additions & 112 deletions

File tree

.devbox/gen/scripts/.hooks.sh

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,4 +6,4 @@ echo -n 'golangci-lint: ' && golangci-lint --version
66
echo '-------------------------------------------'
77
echo -n 'Docker: ' && (docker info --format '{{.ServerVersion}}' 2>/dev/null || echo 'nicht erreichbar – Integrationstests werden fehlschlagen')
88
echo '-------------------------------------------'
9-
echo 'Skripte: build | test | test-gateway | test-worker | test-admin | test-bounce | test-integration | lint | coverage | generate | up | down'
9+
echo 'Skripte: build | test | test-* | lint | coverage | coverage-html | mutate | metrics | sonar | generate | up | down | up-proxy | down-proxy | run-worker-dev | run-gateway-dev'

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,3 +2,5 @@
22
coverage.out
33
*.test
44
/bin/
5+
.env
6+
kladde.md

.scannerwork/.sonar_lock

Whitespace-only changes.

.scannerwork/report-task.txt

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
projectKey=dispatch
2+
serverUrl=http://10.27.27.202:9000
3+
serverVersion=26.4.0.121862
4+
dashboardUrl=http://10.27.27.202:9000/dashboard?id=dispatch
5+
ceTaskId=f6d4b0a5-29bb-480d-8727-32f681318ba9
6+
ceTaskUrl=http://10.27.27.202:9000/api/ce/task?id=f6d4b0a5-29bb-480d-8727-32f681318ba9

devbox.json

Lines changed: 28 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,16 @@
33
"go_1_25",
44
"golangci-lint",
55
"gotools",
6-
"nats-server"
6+
"nats-server",
7+
"sonar-scanner-cli",
8+
"jdk21@latest"
79
],
810
"env": {
911
"DOCKER_API_VERSION": "1.41",
1012
"TESTCONTAINERS_RYUK_DISABLED": "true",
1113
"GOFLAGS": "-mod=mod",
12-
"DISPATCH_ENV": "dev"
14+
"DISPATCH_ENV": "dev",
15+
"JAVA_HOME": "${DEVBOX_PACKAGES_DIR}/openjdk-21"
1316
},
1417
"shell": {
1518
"init_hook": [
@@ -21,29 +24,30 @@
2124
"echo '-------------------------------------------'",
2225
"echo -n 'Docker: ' && (docker info --format '{{.ServerVersion}}' 2>/dev/null || echo 'nicht erreichbar – Integrationstests werden fehlschlagen')",
2326
"echo '-------------------------------------------'",
24-
"echo 'Skripte: build | test | test-* | lint | coverage | coverage-html | mutate | metrics | generate | up | down | up-proxy | down-proxy | run-worker-dev | run-gateway-dev'"
27+
"echo 'Skripte: build | test | test-* | lint | coverage | coverage-html | mutate | metrics | sonar | generate | up | down | up-proxy | down-proxy | run-worker-dev | run-gateway-dev'"
2528
],
2629
"scripts": {
27-
"build": "go build ./...",
28-
"test": "go test ./...",
29-
"test-gateway": "go test ./cmd/mail-gateway/... ./internal/gateway/...",
30-
"test-worker": "go test ./cmd/mail-worker/... ./internal/worker/...",
31-
"test-admin": "go test ./cmd/mail-admin/... ./internal/admin/...",
32-
"test-bounce": "go test ./cmd/bouncemanagement/... ./internal/bounce/...",
33-
"test-integration":"go test -tags integration -timeout 120s ./...",
34-
"lint": "golangci-lint run ./...",
35-
"generate": "go generate ./...",
36-
"up": "docker compose up -d && echo 'NATS gestartet. Warte auf Readiness...' && sleep 2 && docker compose ps",
37-
"down": "docker compose down",
38-
"up-proxy": "docker compose --profile proxy up -d && echo 'NATS + Dev Proxy gestartet (http://localhost:8000)' && sleep 3 && docker compose --profile proxy ps",
39-
"down-proxy": "docker compose --profile proxy down",
40-
"run-worker-dev": "MS_GRAPH_MOCK_TOKEN=dev-token MS_GRAPH_PROXY_URL=http://localhost:8000 NATS_URL=nats://localhost:4222 go run ./cmd/mail-worker",
41-
"run-gateway-dev": "NATS_URL=nats://localhost:4222 MS_GRAPH_MOCK_TOKEN=dev-token go run ./cmd/mail-gateway",
42-
"coverage": "go test -coverprofile=coverage.out -covermode=atomic ./... && devbox run coverage-report",
43-
"coverage-report": "go tool cover -func=coverage.out | awk 'BEGIN{print \"\\nCoverage-Report\"} /^total/{printf \"%-60s %s\\n\", $1, $3; next} {printf \"%-60s %s\\n\", $1, $3}' | tail -30",
44-
"coverage-html": "go test -coverprofile=coverage.out -covermode=atomic ./... && go tool cover -html=coverage.out -o coverage.html && echo 'HTML-Report: coverage.html'",
45-
"mutate": "go tool gremlins unleash --workers 4 --timeout-coefficient 20 ./internal/gateway && go tool gremlins unleash --workers 4 --timeout-coefficient 20 ./internal/quota && go tool gremlins unleash --workers 4 --timeout-coefficient 20 ./internal/spam && go tool gremlins unleash --workers 4 --timeout-coefficient 20 ./internal/worker && go tool gremlins unleash --workers 4 --timeout-coefficient 20 ./internal/pii && go tool gremlins unleash --workers 4 --timeout-coefficient 20 ./internal/hash && go tool gremlins unleash --workers 4 --timeout-coefficient 20 ./internal/msgraph",
46-
"metrics": "devbox run coverage && devbox run mutate"
30+
"build": "go build ./...",
31+
"test": "go test ./...",
32+
"test-gateway": "go test ./cmd/mail-gateway/... ./internal/gateway/...",
33+
"test-worker": "go test ./cmd/mail-worker/... ./internal/worker/...",
34+
"test-admin": "go test ./cmd/mail-admin/... ./internal/admin/...",
35+
"test-bounce": "go test ./cmd/bouncemanagement/... ./internal/bounce/...",
36+
"test-integration": "go test -tags integration -timeout 120s ./...",
37+
"lint": "golangci-lint run ./...",
38+
"generate": "go generate ./...",
39+
"up": "docker compose up -d && echo 'NATS gestartet. Warte auf Readiness...' && sleep 2 && docker compose ps",
40+
"down": "docker compose down",
41+
"up-proxy": "docker compose --profile proxy up -d && echo 'NATS + Dev Proxy gestartet (http://localhost:8000)' && sleep 3 && docker compose --profile proxy ps",
42+
"down-proxy": "docker compose --profile proxy down",
43+
"run-worker-dev": "MS_GRAPH_MOCK_TOKEN=dev-token MS_GRAPH_PROXY_URL=http://localhost:8000 NATS_URL=nats://localhost:4222 go run ./cmd/mail-worker",
44+
"run-gateway-dev": "NATS_URL=nats://localhost:4222 MS_GRAPH_MOCK_TOKEN=dev-token go run ./cmd/mail-gateway",
45+
"coverage": "go test -coverprofile=coverage.out -covermode=atomic ./... && devbox run coverage-report",
46+
"coverage-report": "go tool cover -func=coverage.out | awk 'BEGIN{print \"\\nCoverage-Report\"} /^total/{printf \"%-60s %s\\n\", $1, $3; next} {printf \"%-60s %s\\n\", $1, $3}' | tail -30",
47+
"coverage-html": "go test -coverprofile=coverage.out -covermode=atomic ./... && go tool cover -html=coverage.out -o coverage.html && echo 'HTML-Report: coverage.html'",
48+
"mutate": "go tool gremlins unleash --workers 4 --timeout-coefficient 20 ./internal/gateway && go tool gremlins unleash --workers 4 --timeout-coefficient 20 ./internal/quota && go tool gremlins unleash --workers 4 --timeout-coefficient 20 ./internal/spam && go tool gremlins unleash --workers 4 --timeout-coefficient 20 ./internal/worker && go tool gremlins unleash --workers 4 --timeout-coefficient 20 ./internal/pii && go tool gremlins unleash --workers 4 --timeout-coefficient 20 ./internal/hash && go tool gremlins unleash --workers 4 --timeout-coefficient 20 ./internal/msgraph",
49+
"metrics": "devbox run coverage && devbox run mutate",
50+
"sonar": "go test -coverprofile=coverage.out -covermode=atomic ./... && set -a && . $DEVBOX_PROJECT_ROOT/.env && set +a && sonar-scanner"
4751
}
4852
}
49-
}
53+
}

devbox.lock

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
{
2+
"lockfile_version": "1",
3+
"packages": {
4+
"github:NixOS/nixpkgs/nixpkgs-unstable": {
5+
"last_modified": "2026-04-29T01:19:07Z",
6+
"resolved": "github:NixOS/nixpkgs/ebc08544afa77957cc348ba72dc490ec73b87f68?lastModified=1777425547&narHash=sha256-d57AbflkNfZNoFaZDzssEq1RfPoM9dLtOGI2O%2BN%2F68Q%3D"
7+
},
8+
"go_1_25": {
9+
"resolved": "github:NixOS/nixpkgs/b86751bc4085f48661017fa226dee99fab6c651b?narHash=sha256-a8BYi3mzoJ%2FAcJP8UldOx8emoPRLeWqALZWu4ZvjPXw%3D#go_1_25",
10+
"source": "nixpkg"
11+
},
12+
"golangci-lint": {
13+
"resolved": "github:NixOS/nixpkgs/b86751bc4085f48661017fa226dee99fab6c651b?narHash=sha256-a8BYi3mzoJ%2FAcJP8UldOx8emoPRLeWqALZWu4ZvjPXw%3D#golangci-lint",
14+
"source": "nixpkg"
15+
},
16+
"gotools": {
17+
"resolved": "github:NixOS/nixpkgs/b86751bc4085f48661017fa226dee99fab6c651b?narHash=sha256-a8BYi3mzoJ%2FAcJP8UldOx8emoPRLeWqALZWu4ZvjPXw%3D#gotools",
18+
"source": "nixpkg"
19+
},
20+
"jdk21@latest": {
21+
"last_modified": "2024-06-12T20:55:33Z",
22+
"resolved": "github:NixOS/nixpkgs/a9858885e197f984d92d7fe64e9fff6b2e488d40#jdk",
23+
"source": "devbox-search",
24+
"version": "21+35",
25+
"systems": {
26+
"aarch64-linux": {
27+
"outputs": [
28+
{
29+
"name": "out",
30+
"path": "/nix/store/f7risq9mlz4gywxr65v46h4mv1a4a0s3-openjdk-21+35",
31+
"default": true
32+
},
33+
{
34+
"name": "debug",
35+
"path": "/nix/store/lgljxsffciqlijp38iklrh1ki5vi0y4f-openjdk-21+35-debug"
36+
}
37+
],
38+
"store_path": "/nix/store/f7risq9mlz4gywxr65v46h4mv1a4a0s3-openjdk-21+35"
39+
},
40+
"x86_64-linux": {
41+
"outputs": [
42+
{
43+
"name": "out",
44+
"path": "/nix/store/bk3x3ia7gxqic1jgr3dz05gwi8zw671y-openjdk-21+35",
45+
"default": true
46+
},
47+
{
48+
"name": "debug",
49+
"path": "/nix/store/2h6zm6qy04amw9mvir8n5fifn99dzg16-openjdk-21+35-debug"
50+
}
51+
],
52+
"store_path": "/nix/store/bk3x3ia7gxqic1jgr3dz05gwi8zw671y-openjdk-21+35"
53+
}
54+
}
55+
},
56+
"nats-server": {
57+
"resolved": "github:NixOS/nixpkgs/b86751bc4085f48661017fa226dee99fab6c651b?narHash=sha256-a8BYi3mzoJ%2FAcJP8UldOx8emoPRLeWqALZWu4ZvjPXw%3D#nats-server",
58+
"source": "nixpkg"
59+
},
60+
"sonar-scanner-cli": {
61+
"resolved": "github:NixOS/nixpkgs/ebc08544afa77957cc348ba72dc490ec73b87f68?narHash=sha256-d57AbflkNfZNoFaZDzssEq1RfPoM9dLtOGI2O%2BN%2F68Q%3D#sonar-scanner-cli",
62+
"source": "nixpkg"
63+
}
64+
}
65+
}

internal/admin/resolver.go

Lines changed: 18 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -64,18 +64,9 @@ func (r *Resolver) Mails(ctx context.Context, args pagedMailArgs) (*pagedMailRes
6464

6565
filtered := records[:0]
6666
for _, rec := range records {
67-
if args.Filter != nil {
68-
if args.Filter.AppTag != nil && rec.AppTag != *args.Filter.AppTag {
69-
continue
70-
}
71-
if args.Filter.Status != nil && rec.Status != *args.Filter.Status {
72-
continue
73-
}
74-
if args.Filter.TraceID != nil && rec.TraceID != *args.Filter.TraceID {
75-
continue
76-
}
67+
if matchesMailFilter(rec, args.Filter) {
68+
filtered = append(filtered, rec)
7769
}
78-
filtered = append(filtered, rec)
7970
}
8071

8172
page, size := pageSize(args.Page, args.Size)
@@ -165,6 +156,22 @@ func (r *Resolver) ReprocessDeadLetter(ctx context.Context, args struct{ Payload
165156
return true, nil
166157
}
167158

159+
func matchesMailFilter(rec domain.AuditRecord, f *mailFilterArgs) bool {
160+
if f == nil {
161+
return true
162+
}
163+
if f.AppTag != nil && rec.AppTag != *f.AppTag {
164+
return false
165+
}
166+
if f.Status != nil && rec.Status != *f.Status {
167+
return false
168+
}
169+
if f.TraceID != nil && rec.TraceID != *f.TraceID {
170+
return false
171+
}
172+
return true
173+
}
174+
168175
// --- stream helpers ---
169176

170177
func (r *Resolver) readAuditStream(_ context.Context) ([]domain.AuditRecord, error) {

internal/config/config_test.go

Lines changed: 22 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,19 @@ import (
55
"time"
66
)
77

8+
const (
9+
testNatsURL = "nats://localhost:4222"
10+
testSenderEmail = "sender@example.com"
11+
unexpectedErr = "unexpected error: %v"
12+
)
13+
814
func setRequiredEnv(t *testing.T) {
915
t.Helper()
10-
t.Setenv("NATS_URL", "nats://localhost:4222")
16+
t.Setenv("NATS_URL", testNatsURL)
1117
t.Setenv("MS_GRAPH_TENANT_ID", "tenant")
1218
t.Setenv("MS_GRAPH_CLIENT_ID", "client")
1319
t.Setenv("MS_GRAPH_CLIENT_SECRET", "secret")
14-
t.Setenv("MS_GRAPH_SENDER_EMAIL", "sender@example.com")
20+
t.Setenv("MS_GRAPH_SENDER_EMAIL", testSenderEmail)
1521
}
1622

1723
func TestLoad_Success(t *testing.T) {
@@ -20,10 +26,10 @@ func TestLoad_Success(t *testing.T) {
2026
if err != nil {
2127
t.Fatalf("expected no error, got %v", err)
2228
}
23-
if cfg.NatsURL != "nats://localhost:4222" {
29+
if cfg.NatsURL != testNatsURL {
2430
t.Errorf("NatsURL: got %s", cfg.NatsURL)
2531
}
26-
if cfg.MSGraphSenderEmail != "sender@example.com" {
32+
if cfg.MSGraphSenderEmail != testSenderEmail {
2733
t.Errorf("MSGraphSenderEmail: got %s", cfg.MSGraphSenderEmail)
2834
}
2935
}
@@ -32,7 +38,7 @@ func TestLoad_Defaults(t *testing.T) {
3238
setRequiredEnv(t)
3339
cfg, err := Load()
3440
if err != nil {
35-
t.Fatalf("unexpected error: %v", err)
41+
t.Fatalf(unexpectedErr, err)
3642
}
3743
if cfg.Port != "8080" {
3844
t.Errorf("Port default: want 8080, got %s", cfg.Port)
@@ -59,7 +65,7 @@ func TestLoad_MissingNatsURL(t *testing.T) {
5965
t.Setenv("MS_GRAPH_TENANT_ID", "tenant")
6066
t.Setenv("MS_GRAPH_CLIENT_ID", "client")
6167
t.Setenv("MS_GRAPH_CLIENT_SECRET", "secret")
62-
t.Setenv("MS_GRAPH_SENDER_EMAIL", "sender@example.com")
68+
t.Setenv("MS_GRAPH_SENDER_EMAIL", testSenderEmail)
6369

6470
_, err := Load()
6571
if err == nil {
@@ -68,7 +74,7 @@ func TestLoad_MissingNatsURL(t *testing.T) {
6874
}
6975

7076
func TestLoad_MissingGraphCredentials(t *testing.T) {
71-
t.Setenv("NATS_URL", "nats://localhost:4222")
77+
t.Setenv("NATS_URL", testNatsURL)
7278
// MS_GRAPH_MOCK_TOKEN is not set → credentials are required
7379

7480
cases := []struct {
@@ -87,7 +93,7 @@ func TestLoad_MissingGraphCredentials(t *testing.T) {
8793
t.Setenv("MS_GRAPH_TENANT_ID", "tenant")
8894
t.Setenv("MS_GRAPH_CLIENT_ID", "client")
8995
t.Setenv("MS_GRAPH_CLIENT_SECRET", "secret")
90-
t.Setenv("MS_GRAPH_SENDER_EMAIL", "sender@example.com")
96+
t.Setenv("MS_GRAPH_SENDER_EMAIL", testSenderEmail)
9197
t.Setenv(tc.missing, "")
9298

9399
_, err := Load()
@@ -99,7 +105,7 @@ func TestLoad_MissingGraphCredentials(t *testing.T) {
99105
}
100106

101107
func TestLoad_MockTokenSkipsCredentialCheck(t *testing.T) {
102-
t.Setenv("NATS_URL", "nats://localhost:4222")
108+
t.Setenv("NATS_URL", testNatsURL)
103109
t.Setenv("MS_GRAPH_MOCK_TOKEN", "dev-token")
104110
// deliberately leave Graph credentials unset
105111

@@ -122,7 +128,7 @@ func TestLoad_IntEnvOverrides(t *testing.T) {
122128

123129
cfg, err := Load()
124130
if err != nil {
125-
t.Fatalf("unexpected error: %v", err)
131+
t.Fatalf(unexpectedErr, err)
126132
}
127133
if cfg.SpamTimeoutSeconds != 30 {
128134
t.Errorf("SpamTimeoutSeconds: want 30, got %d", cfg.SpamTimeoutSeconds)
@@ -147,7 +153,7 @@ func TestLoad_InvalidIntFallsToDefault(t *testing.T) {
147153

148154
cfg, err := Load()
149155
if err != nil {
150-
t.Fatalf("unexpected error: %v", err)
156+
t.Fatalf(unexpectedErr, err)
151157
}
152158
if cfg.SpamTimeoutSeconds != 60 {
153159
t.Errorf("expected default 60 on parse error, got %d", cfg.SpamTimeoutSeconds)
@@ -159,9 +165,9 @@ func TestLoad_BounceMailboxDefaultsToSenderEmail(t *testing.T) {
159165

160166
cfg, err := Load()
161167
if err != nil {
162-
t.Fatalf("unexpected error: %v", err)
168+
t.Fatalf(unexpectedErr, err)
163169
}
164-
if cfg.MSGraphBounceMailbox != "sender@example.com" {
170+
if cfg.MSGraphBounceMailbox != testSenderEmail {
165171
t.Errorf("BounceMailbox: want sender@example.com, got %s", cfg.MSGraphBounceMailbox)
166172
}
167173
}
@@ -172,7 +178,7 @@ func TestLoad_BounceMailboxOverride(t *testing.T) {
172178

173179
cfg, err := Load()
174180
if err != nil {
175-
t.Fatalf("unexpected error: %v", err)
181+
t.Fatalf(unexpectedErr, err)
176182
}
177183
if cfg.MSGraphBounceMailbox != "bounces@example.com" {
178184
t.Errorf("BounceMailbox: want bounces@example.com, got %s", cfg.MSGraphBounceMailbox)
@@ -185,7 +191,7 @@ func TestLoad_ProxyURL(t *testing.T) {
185191

186192
cfg, err := Load()
187193
if err != nil {
188-
t.Fatalf("unexpected error: %v", err)
194+
t.Fatalf(unexpectedErr, err)
189195
}
190196
if cfg.GraphProxyURL != "http://localhost:8000" {
191197
t.Errorf("GraphProxyURL: want http://localhost:8000, got %s", cfg.GraphProxyURL)
@@ -198,7 +204,7 @@ func TestLoad_MimeWhitelistOverride(t *testing.T) {
198204

199205
cfg, err := Load()
200206
if err != nil {
201-
t.Fatalf("unexpected error: %v", err)
207+
t.Fatalf(unexpectedErr, err)
202208
}
203209
if len(cfg.MimeWhitelist) != 2 {
204210
t.Fatalf("MimeWhitelist: want 2 entries, got %d", len(cfg.MimeWhitelist))

internal/domain/errors_test.go

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@ import (
55
"testing"
66
)
77

8+
const errMustNotBeEmpty = "Error() must not return empty string"
9+
810
func TestValidationError_Error(t *testing.T) {
911
e := &ValidationError{Code: ErrUnknownAppTag, Message: "not found"}
1012
want := "UNKNOWN_APP_TAG: not found"
@@ -17,15 +19,15 @@ func TestQuotaError_Error(t *testing.T) {
1719
e := &QuotaError{Limit: 100, Current: 95, Requested: 10}
1820
got := e.Error()
1921
if got == "" {
20-
t.Fatal("Error() must not return empty string")
22+
t.Fatal(errMustNotBeEmpty)
2123
}
2224
}
2325

2426
func TestQuotaStateError_Error(t *testing.T) {
2527
cause := errors.New("nats down")
2628
e := &QuotaStateError{Cause: cause}
2729
if e.Error() == "" {
28-
t.Fatal("Error() must not return empty string")
30+
t.Fatal(errMustNotBeEmpty)
2931
}
3032
if !errors.Is(e, cause) {
3133
t.Error("Unwrap must expose cause for errors.Is")
@@ -36,7 +38,7 @@ func TestNatsPublishError_Error(t *testing.T) {
3638
cause := errors.New("connection refused")
3739
e := &NatsPublishError{Cause: cause}
3840
if e.Error() == "" {
39-
t.Fatal("Error() must not return empty string")
41+
t.Fatal(errMustNotBeEmpty)
4042
}
4143
if !errors.Is(e, cause) {
4244
t.Error("Unwrap must expose cause for errors.Is")

0 commit comments

Comments
 (0)