dms: apierror, httpclient, tenantregistryclient aus common v0.1.0 #384
No files matched your search
+1
-1
@@ -15,6 +15,7 @@ import (
|
||||
|
||||
"github.com/jackc/pgx/v5/pgxpool"
|
||||
|
||||
"gitea.perlbach24.de/scripte/nexarch/common/tenantregistryclient"
|
||||
"gitea.perlbach24.de/scripte/nexarch/dms/internal/audittrail"
|
||||
"gitea.perlbach24.de/scripte/nexarch/dms/internal/authz"
|
||||
"gitea.perlbach24.de/scripte/nexarch/dms/internal/batchsplit"
|
||||
@@ -34,7 +35,6 @@ import (
|
||||
"gitea.perlbach24.de/scripte/nexarch/dms/internal/searchwiring"
|
||||
"gitea.perlbach24.de/scripte/nexarch/dms/internal/shared"
|
||||
"gitea.perlbach24.de/scripte/nexarch/dms/internal/storage"
|
||||
"gitea.perlbach24.de/scripte/nexarch/dms/internal/tenantregistryclient"
|
||||
"gitea.perlbach24.de/scripte/nexarch/dms/internal/watchfolder"
|
||||
)
|
||||
|
||||
|
||||
@@ -30,9 +30,9 @@ import (
|
||||
|
||||
"github.com/jackc/pgx/v5/pgxpool"
|
||||
|
||||
"gitea.perlbach24.de/scripte/nexarch/common/httpclient"
|
||||
dmscrypto "gitea.perlbach24.de/scripte/nexarch/dms/internal/crypto"
|
||||
"gitea.perlbach24.de/scripte/nexarch/dms/internal/folder"
|
||||
"gitea.perlbach24.de/scripte/nexarch/dms/internal/httpclient"
|
||||
"gitea.perlbach24.de/scripte/nexarch/dms/internal/jobqueue"
|
||||
"gitea.perlbach24.de/scripte/nexarch/dms/internal/migrate"
|
||||
"gitea.perlbach24.de/scripte/nexarch/dms/internal/routeprefix"
|
||||
|
||||
@@ -23,8 +23,8 @@ import (
|
||||
|
||||
"github.com/jackc/pgx/v5/pgxpool"
|
||||
|
||||
"gitea.perlbach24.de/scripte/nexarch/common/httpclient"
|
||||
dmscrypto "gitea.perlbach24.de/scripte/nexarch/dms/internal/crypto"
|
||||
"gitea.perlbach24.de/scripte/nexarch/dms/internal/httpclient"
|
||||
"gitea.perlbach24.de/scripte/nexarch/dms/internal/jobqueue"
|
||||
"gitea.perlbach24.de/scripte/nexarch/dms/internal/migrate"
|
||||
"gitea.perlbach24.de/scripte/nexarch/dms/internal/routeprefix"
|
||||
|
||||
@@ -5,6 +5,7 @@ go 1.25.0
|
||||
toolchain go1.25.13
|
||||
|
||||
require (
|
||||
gitea.perlbach24.de/scripte/nexarch/common v0.1.0
|
||||
github.com/aws/aws-sdk-go-v2 v1.45.1
|
||||
github.com/aws/aws-sdk-go-v2/config v1.33.1
|
||||
github.com/aws/aws-sdk-go-v2/credentials v1.20.1
|
||||
|
||||
+6
-4
@@ -1,5 +1,7 @@
|
||||
filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA=
|
||||
filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=
|
||||
gitea.perlbach24.de/scripte/nexarch/common v0.1.0 h1:biNzFr2LOMZC7b1leY3smQtrMmSMp6IW80OvR4LSjLY=
|
||||
gitea.perlbach24.de/scripte/nexarch/common v0.1.0/go.mod h1:CiEImG++QMt1xZqSMeDJhPiB7sfWTLZOwjS6OY/Iguc=
|
||||
github.com/aws/aws-sdk-go-v2 v1.45.1 h1:iIoG3NaLhV6UZpPXyPXlDj2I9oS8tV/nMcMnITCC6Ks=
|
||||
github.com/aws/aws-sdk-go-v2 v1.45.1/go.mod h1:bttEH6JqnUL8LepvDVfdrds/fZ5bCIxzpe3abyUrhDU=
|
||||
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.20 h1:GPRlPwz40I2B2VrBEASOA3Bi77NyeqejNLkifosX0rs=
|
||||
@@ -81,10 +83,10 @@ github.com/pdfcpu/pdfcpu v0.15.0/go.mod h1:NhG6T7b2EEdToXGD5hj8rmXBWSLCjgljCk5c0
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
|
||||
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
|
||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
|
||||
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
|
||||
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
||||
go.yaml.in/yaml/v3 v3.0.5 h1:N6y/pJk8buWs9NY5ERU2HSMfm+IuD/OtfdAnq6kESPw=
|
||||
go.yaml.in/yaml/v3 v3.0.5/go.mod h1:HVTZu1O7/Vkt2N+BFy8Zza+lnLsABggaTM2ZpNIGuKg=
|
||||
@@ -114,14 +116,14 @@ golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
|
||||
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||
golang.org/x/text v0.41.0 h1:vz/seA0lnX87Othu2f/0L24RcgrXD9/YFTSuGjj3rH8=
|
||||
golang.org/x/text v0.41.0/go.mod h1:jvf1O8ajNzZqhSrQBPbutR/EB83Cc0CFrezNQIwbb5M=
|
||||
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
||||
|
||||
@@ -1,149 +0,0 @@
|
||||
// Package apierror erzwingt das Core-Fehlerformat (API-01) auf allen DMS-Endpunkten.
|
||||
package apierror
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func CodeForStatus(status int) string {
|
||||
switch status {
|
||||
case http.StatusBadRequest:
|
||||
return "invalid_request"
|
||||
case http.StatusUnauthorized:
|
||||
return "unauthenticated"
|
||||
case http.StatusForbidden:
|
||||
return "forbidden"
|
||||
case http.StatusNotFound:
|
||||
return "not_found"
|
||||
case http.StatusMethodNotAllowed:
|
||||
return "method_not_allowed"
|
||||
case http.StatusConflict:
|
||||
return "conflict"
|
||||
case http.StatusGone:
|
||||
return "gone"
|
||||
case http.StatusRequestEntityTooLarge:
|
||||
return "payload_too_large"
|
||||
case http.StatusUnsupportedMediaType:
|
||||
return "unsupported_media_type"
|
||||
case http.StatusUnprocessableEntity:
|
||||
return "unprocessable"
|
||||
case http.StatusLocked:
|
||||
return "locked"
|
||||
case http.StatusTooManyRequests:
|
||||
return "rate_limited"
|
||||
case http.StatusServiceUnavailable:
|
||||
return "unavailable"
|
||||
}
|
||||
if status >= 500 {
|
||||
return "internal_error"
|
||||
}
|
||||
return "invalid_request"
|
||||
}
|
||||
|
||||
type body struct {
|
||||
Error struct {
|
||||
Code string `json:"code"`
|
||||
Message string `json:"message"`
|
||||
} `json:"error"`
|
||||
}
|
||||
|
||||
func Write(w http.ResponseWriter, status int, code, message string) {
|
||||
var b body
|
||||
b.Error.Code = code
|
||||
b.Error.Message = message
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(status)
|
||||
_ = json.NewEncoder(w).Encode(b)
|
||||
}
|
||||
|
||||
// Normalize schreibt jede Fehlerantwort (Status >= 400) im Core-Format um;
|
||||
// Erfolgsantworten laufen ungepuffert durch.
|
||||
func Normalize(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
rec := &recorder{ResponseWriter: w}
|
||||
next.ServeHTTP(rec, r)
|
||||
rec.finish()
|
||||
})
|
||||
}
|
||||
|
||||
type recorder struct {
|
||||
http.ResponseWriter
|
||||
status int
|
||||
buffered bool
|
||||
buf bytes.Buffer
|
||||
}
|
||||
|
||||
func (r *recorder) WriteHeader(status int) {
|
||||
if r.status != 0 {
|
||||
return
|
||||
}
|
||||
r.status = status
|
||||
if status >= 400 {
|
||||
r.buffered = true
|
||||
return
|
||||
}
|
||||
r.ResponseWriter.WriteHeader(status)
|
||||
}
|
||||
|
||||
func (r *recorder) Write(p []byte) (int, error) {
|
||||
if r.status == 0 {
|
||||
r.WriteHeader(http.StatusOK)
|
||||
}
|
||||
if r.buffered {
|
||||
return r.buf.Write(p)
|
||||
}
|
||||
return r.ResponseWriter.Write(p)
|
||||
}
|
||||
|
||||
func (r *recorder) Flush() {
|
||||
if f, ok := r.ResponseWriter.(http.Flusher); ok && !r.buffered {
|
||||
f.Flush()
|
||||
}
|
||||
}
|
||||
|
||||
func (r *recorder) Unwrap() http.ResponseWriter { return r.ResponseWriter }
|
||||
|
||||
func (r *recorder) finish() {
|
||||
if !r.buffered {
|
||||
return
|
||||
}
|
||||
out := map[string]any{}
|
||||
message := strings.TrimSpace(r.buf.String())
|
||||
code := CodeForStatus(r.status)
|
||||
|
||||
var parsed map[string]any
|
||||
if json.Unmarshal(r.buf.Bytes(), &parsed) == nil && parsed != nil {
|
||||
switch e := parsed["error"].(type) {
|
||||
case string:
|
||||
message = e
|
||||
delete(parsed, "error")
|
||||
out = parsed
|
||||
case map[string]any:
|
||||
if c, _ := e["code"].(string); c != "" {
|
||||
if m, _ := e["message"].(string); m != "" {
|
||||
r.flush(r.buf.Bytes())
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if message == "" {
|
||||
message = http.StatusText(r.status)
|
||||
}
|
||||
out["error"] = map[string]string{"code": code, "message": message}
|
||||
encoded, _ := json.Marshal(out)
|
||||
r.flush(append(encoded, '\n'))
|
||||
}
|
||||
|
||||
func (r *recorder) flush(payload []byte) {
|
||||
h := r.ResponseWriter.Header()
|
||||
h.Set("Content-Type", "application/json")
|
||||
h.Del("Content-Length")
|
||||
h.Del("X-Content-Type-Options")
|
||||
h.Set("X-Content-Type-Options", "nosniff")
|
||||
r.ResponseWriter.WriteHeader(r.status)
|
||||
_, _ = r.ResponseWriter.Write(payload)
|
||||
}
|
||||
@@ -1,118 +0,0 @@
|
||||
package apierror
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func decode(t *testing.T, rec *httptest.ResponseRecorder) map[string]any {
|
||||
t.Helper()
|
||||
var m map[string]any
|
||||
if err := json.Unmarshal(rec.Body.Bytes(), &m); err != nil {
|
||||
t.Fatalf("keine json-antwort: %q (%v)", rec.Body.String(), err)
|
||||
}
|
||||
return m
|
||||
}
|
||||
|
||||
func errOf(t *testing.T, m map[string]any) (string, string) {
|
||||
t.Helper()
|
||||
e, ok := m["error"].(map[string]any)
|
||||
if !ok {
|
||||
t.Fatalf("error ist kein objekt: %v", m)
|
||||
}
|
||||
c, _ := e["code"].(string)
|
||||
msg, _ := e["message"].(string)
|
||||
return c, msg
|
||||
}
|
||||
|
||||
func TestNormalizeRewritesLegacyErrorShapes(t *testing.T) {
|
||||
cases := []struct {
|
||||
name string
|
||||
handler http.HandlerFunc
|
||||
status int
|
||||
wantCode string
|
||||
wantMsg string
|
||||
}{
|
||||
{"http.Error klartext", func(w http.ResponseWriter, r *http.Request) { http.Error(w, "dokument nicht gefunden", 404) }, 404, "not_found", "dokument nicht gefunden"},
|
||||
{"json string", func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(422)
|
||||
_, _ = w.Write([]byte(`{"error":"ungueltiger wert"}`))
|
||||
}, 422, "unprocessable", "ungueltiger wert"},
|
||||
{"leerer body", func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(403) }, 403, "forbidden", "Forbidden"},
|
||||
{"5xx", func(w http.ResponseWriter, r *http.Request) { http.Error(w, "boom", 500) }, 500, "internal_error", "boom"},
|
||||
}
|
||||
for _, c := range cases {
|
||||
t.Run(c.name, func(t *testing.T) {
|
||||
rec := httptest.NewRecorder()
|
||||
Normalize(c.handler).ServeHTTP(rec, httptest.NewRequest("GET", "/x", nil))
|
||||
if rec.Code != c.status {
|
||||
t.Fatalf("status = %d", rec.Code)
|
||||
}
|
||||
if ct := rec.Header().Get("Content-Type"); !strings.HasPrefix(ct, "application/json") {
|
||||
t.Errorf("content-type = %q", ct)
|
||||
}
|
||||
code, msg := errOf(t, decode(t, rec))
|
||||
if code != c.wantCode || msg != c.wantMsg {
|
||||
t.Errorf("code=%q msg=%q, erwartet %q/%q", code, msg, c.wantCode, c.wantMsg)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestNormalizeKeepsExtraFieldsAndCoreShape(t *testing.T) {
|
||||
rec := httptest.NewRecorder()
|
||||
Normalize(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(409)
|
||||
_, _ = w.Write([]byte(`{"error":"duplikat","documentId":"d1"}`))
|
||||
})).ServeHTTP(rec, httptest.NewRequest("POST", "/x", nil))
|
||||
m := decode(t, rec)
|
||||
if m["documentId"] != "d1" {
|
||||
t.Errorf("zusatzfeld verloren: %v", m)
|
||||
}
|
||||
if c, _ := errOf(t, m); c != "conflict" {
|
||||
t.Errorf("code = %q", c)
|
||||
}
|
||||
|
||||
rec = httptest.NewRecorder()
|
||||
Normalize(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
Write(w, 401, "unauthenticated", "nicht angemeldet")
|
||||
})).ServeHTTP(rec, httptest.NewRequest("GET", "/x", nil))
|
||||
if c, m := errOf(t, decode(t, rec)); c != "unauthenticated" || m != "nicht angemeldet" {
|
||||
t.Errorf("core-format veraendert: %q %q", c, m)
|
||||
}
|
||||
}
|
||||
|
||||
func TestNormalizeSuccessPassesThroughUnbuffered(t *testing.T) {
|
||||
rec := httptest.NewRecorder()
|
||||
Normalize(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "application/pdf")
|
||||
_, _ = w.Write([]byte("%PDF-1.7"))
|
||||
})).ServeHTTP(rec, httptest.NewRequest("GET", "/x", nil))
|
||||
if rec.Code != 200 || rec.Body.String() != "%PDF-1.7" || rec.Header().Get("Content-Type") != "application/pdf" {
|
||||
t.Errorf("erfolgsantwort veraendert: %d %q %q", rec.Code, rec.Body.String(), rec.Header().Get("Content-Type"))
|
||||
}
|
||||
}
|
||||
|
||||
func TestNormalizeMuxNotFoundAndMethodNotAllowed(t *testing.T) {
|
||||
mux := http.NewServeMux()
|
||||
mux.HandleFunc("GET /a", func(w http.ResponseWriter, r *http.Request) {})
|
||||
h := Normalize(mux)
|
||||
for _, c := range []struct {
|
||||
method, path string
|
||||
status int
|
||||
code string
|
||||
}{{"GET", "/nope", 404, "not_found"}, {"POST", "/a", 405, "method_not_allowed"}} {
|
||||
rec := httptest.NewRecorder()
|
||||
h.ServeHTTP(rec, httptest.NewRequest(c.method, c.path, nil))
|
||||
if rec.Code != c.status {
|
||||
t.Fatalf("%s %s: status %d", c.method, c.path, rec.Code)
|
||||
}
|
||||
if code, _ := errOf(t, decode(t, rec)); code != c.code {
|
||||
t.Errorf("%s %s: code %q", c.method, c.path, code)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -9,7 +9,7 @@ import (
|
||||
|
||||
"github.com/jackc/pgx/v5/pgxpool"
|
||||
|
||||
"gitea.perlbach24.de/scripte/nexarch/dms/internal/apierror"
|
||||
"gitea.perlbach24.de/scripte/nexarch/common/apierror"
|
||||
"gitea.perlbach24.de/scripte/nexarch/dms/internal/auditapi"
|
||||
"gitea.perlbach24.de/scripte/nexarch/dms/internal/autoruleapi"
|
||||
"gitea.perlbach24.de/scripte/nexarch/dms/internal/bulkedit"
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
package archtest
|
||||
|
||||
import (
|
||||
"io/fs"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// Architekturtest: im Produktivcode dieses Moduls gibt es keine Aufrufe ueber
|
||||
// den Standard-Client ohne Zeitlimit.
|
||||
func TestNoUnboundedDefaultClientInProductionCode(t *testing.T) {
|
||||
root, err := filepath.Abs(filepath.Join("..", ".."))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
forbidden := regexp.MustCompile(`http\.DefaultClient|http\.(Get|Post|Head|PostForm)\(`)
|
||||
err = filepath.WalkDir(root, func(path string, d fs.DirEntry, err error) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if d.IsDir() {
|
||||
if n := d.Name(); n == "node_modules" || n == "testdata" || n == ".git" {
|
||||
return filepath.SkipDir
|
||||
}
|
||||
return nil
|
||||
}
|
||||
if !strings.HasSuffix(path, ".go") || strings.HasSuffix(path, "_test.go") {
|
||||
return nil
|
||||
}
|
||||
data, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for i, line := range strings.Split(string(data), "\n") {
|
||||
if t := strings.TrimSpace(line); strings.HasPrefix(t, "//") {
|
||||
continue
|
||||
}
|
||||
if forbidden.MatchString(line) {
|
||||
rel, _ := filepath.Rel(root, path)
|
||||
t.Errorf("%s:%d nutzt den Standard-Client ohne Zeitlimit: %s", rel, i+1, strings.TrimSpace(line))
|
||||
}
|
||||
}
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
@@ -5,7 +5,7 @@ import (
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"gitea.perlbach24.de/scripte/nexarch/dms/internal/httpclient"
|
||||
"gitea.perlbach24.de/scripte/nexarch/common/httpclient"
|
||||
"net/http"
|
||||
"net/url"
|
||||
)
|
||||
|
||||
@@ -13,7 +13,7 @@ import (
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"gitea.perlbach24.de/scripte/nexarch/dms/internal/apierror"
|
||||
"gitea.perlbach24.de/scripte/nexarch/common/apierror"
|
||||
"gitea.perlbach24.de/scripte/nexarch/dms/internal/jobqueue"
|
||||
)
|
||||
|
||||
|
||||
@@ -1,29 +0,0 @@
|
||||
// Package httpclient liefert den HTTP-Client fuer ausgehende Dienstaufrufe
|
||||
// (QA-15 F-11): Zeitlimit statt endlosem Warten auf einen haengenden
|
||||
// Nachbardienst, und keine Weiterleitungen (ein Service-Token wuerde sonst an
|
||||
// das Weiterleitungsziel mitgesendet).
|
||||
package httpclient
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"time"
|
||||
)
|
||||
|
||||
// DefaultTimeout gilt fuer einen kompletten Aufruf (Verbindung bis Antwortende).
|
||||
const DefaultTimeout = 15 * time.Second
|
||||
|
||||
var shared = New(DefaultTimeout)
|
||||
|
||||
// Default ist der gemeinsame Client mit DefaultTimeout.
|
||||
func Default() *http.Client { return shared }
|
||||
|
||||
// New liefert einen Client mit dem angegebenen Zeitlimit, der Weiterleitungen
|
||||
// nicht folgt (die 3xx-Antwort wird zurueckgegeben).
|
||||
func New(timeout time.Duration) *http.Client {
|
||||
return &http.Client{
|
||||
Timeout: timeout,
|
||||
CheckRedirect: func(*http.Request, []*http.Request) error {
|
||||
return http.ErrUseLastResponse
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -1,99 +0,0 @@
|
||||
package httpclient
|
||||
|
||||
import (
|
||||
"io/fs"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestDefaultHasTimeout(t *testing.T) {
|
||||
if Default().Timeout != DefaultTimeout || DefaultTimeout <= 0 {
|
||||
t.Fatalf("timeout = %s", Default().Timeout)
|
||||
}
|
||||
}
|
||||
|
||||
func TestClientGivesUpOnHangingServer(t *testing.T) {
|
||||
release := make(chan struct{})
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { <-release }))
|
||||
defer srv.Close()
|
||||
defer close(release)
|
||||
|
||||
start := time.Now()
|
||||
resp, err := New(200 * time.Millisecond).Get(srv.URL)
|
||||
if err == nil {
|
||||
_ = resp.Body.Close()
|
||||
t.Fatal("kein fehler bei haengendem server")
|
||||
}
|
||||
if elapsed := time.Since(start); elapsed > 5*time.Second {
|
||||
t.Fatalf("aufruf dauerte %s", elapsed)
|
||||
}
|
||||
}
|
||||
|
||||
func TestClientDoesNotFollowRedirects(t *testing.T) {
|
||||
target := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
t.Error("weiterleitungsziel wurde aufgerufen (Service-Token-Leck)")
|
||||
}))
|
||||
defer target.Close()
|
||||
redirector := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
http.Redirect(w, r, target.URL, http.StatusTemporaryRedirect)
|
||||
}))
|
||||
defer redirector.Close()
|
||||
|
||||
req, _ := http.NewRequest(http.MethodPost, redirector.URL, strings.NewReader("x"))
|
||||
req.Header.Set("X-Service-Token", "geheim")
|
||||
resp, err := Default().Do(req)
|
||||
if err != nil {
|
||||
t.Fatalf("do: %v", err)
|
||||
}
|
||||
defer func() { _ = resp.Body.Close() }()
|
||||
if resp.StatusCode != http.StatusTemporaryRedirect {
|
||||
t.Fatalf("status = %d, erwartet 307", resp.StatusCode)
|
||||
}
|
||||
}
|
||||
|
||||
// Architekturtest: im Produktivcode dieses Moduls gibt es keine Aufrufe ueber
|
||||
// den Standard-Client ohne Zeitlimit.
|
||||
func TestNoUnboundedDefaultClientInProductionCode(t *testing.T) {
|
||||
root, err := filepath.Abs(filepath.Join("..", ".."))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
forbidden := regexp.MustCompile(`http\.DefaultClient|http\.(Get|Post|Head|PostForm)\(`)
|
||||
err = filepath.WalkDir(root, func(path string, d fs.DirEntry, err error) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if d.IsDir() {
|
||||
if n := d.Name(); n == "node_modules" || n == "testdata" || n == ".git" {
|
||||
return filepath.SkipDir
|
||||
}
|
||||
return nil
|
||||
}
|
||||
if !strings.HasSuffix(path, ".go") || strings.HasSuffix(path, "_test.go") {
|
||||
return nil
|
||||
}
|
||||
data, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for i, line := range strings.Split(string(data), "\n") {
|
||||
if t := strings.TrimSpace(line); strings.HasPrefix(t, "//") {
|
||||
continue
|
||||
}
|
||||
if forbidden.MatchString(line) {
|
||||
rel, _ := filepath.Rel(root, path)
|
||||
t.Errorf("%s:%d nutzt den Standard-Client ohne Zeitlimit: %s", rel, i+1, strings.TrimSpace(line))
|
||||
}
|
||||
}
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
@@ -116,7 +116,7 @@ func setupMigratorTest(t *testing.T) (*Migrator, *pgxpool.Pool, string) {
|
||||
{
|
||||
SourceLinkID: "archivdms-share-3-abgelaufen", SourceDocumentID: "archivdms-doc-1", SourceUserID: "archivdms-user-1",
|
||||
TokenHash: hashToken("abgelaufener-token"),
|
||||
CreatedAt: time.Now().Add(-30 * 24 * time.Hour), ExpiresAt: time.Now().Add(-24 * time.Hour),
|
||||
CreatedAt: time.Now().Add(-30 * 24 * time.Hour), ExpiresAt: time.Now().Add(-24 * time.Hour),
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@ import (
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"gitea.perlbach24.de/scripte/nexarch/dms/internal/apierror"
|
||||
"gitea.perlbach24.de/scripte/nexarch/common/apierror"
|
||||
)
|
||||
|
||||
type Identity struct {
|
||||
|
||||
@@ -12,7 +12,7 @@ import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"gitea.perlbach24.de/scripte/nexarch/dms/internal/httpclient"
|
||||
"gitea.perlbach24.de/scripte/nexarch/common/httpclient"
|
||||
)
|
||||
|
||||
type Client struct {
|
||||
|
||||
@@ -12,7 +12,7 @@ import (
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"gitea.perlbach24.de/scripte/nexarch/dms/internal/apierror"
|
||||
"gitea.perlbach24.de/scripte/nexarch/common/apierror"
|
||||
"gitea.perlbach24.de/scripte/nexarch/dms/internal/moduleauth"
|
||||
"gitea.perlbach24.de/scripte/nexarch/dms/internal/openapi"
|
||||
)
|
||||
|
||||
@@ -13,7 +13,7 @@ import (
|
||||
"net/http"
|
||||
"net/url"
|
||||
|
||||
"gitea.perlbach24.de/scripte/nexarch/dms/internal/httpclient"
|
||||
"gitea.perlbach24.de/scripte/nexarch/common/httpclient"
|
||||
"gitea.perlbach24.de/scripte/nexarch/dms/internal/modulecall"
|
||||
)
|
||||
|
||||
|
||||
@@ -17,7 +17,7 @@ import (
|
||||
|
||||
"github.com/jackc/pgx/v5/pgxpool"
|
||||
|
||||
"gitea.perlbach24.de/scripte/nexarch/dms/internal/apierror"
|
||||
"gitea.perlbach24.de/scripte/nexarch/common/apierror"
|
||||
"gitea.perlbach24.de/scripte/nexarch/dms/internal/audittrail"
|
||||
"gitea.perlbach24.de/scripte/nexarch/dms/internal/jobqueue"
|
||||
"gitea.perlbach24.de/scripte/nexarch/dms/internal/moduleauth"
|
||||
|
||||
@@ -5,7 +5,7 @@ import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"gitea.perlbach24.de/scripte/nexarch/dms/internal/httpclient"
|
||||
"gitea.perlbach24.de/scripte/nexarch/common/httpclient"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
|
||||
@@ -1,46 +0,0 @@
|
||||
// Package tenantregistryclient ist ein schlanker HTTP-Client fuer Core TEN-01
|
||||
// (Tenant-Registry); DMS ist ein eigenes Go-Modul und kann Cores Pakete nicht
|
||||
// importieren. Gleiches Muster wie archive/internal/tenantregistryclient.
|
||||
package tenantregistryclient
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"time"
|
||||
)
|
||||
|
||||
type Client struct {
|
||||
BaseURL string
|
||||
ServiceToken string
|
||||
HTTPClient *http.Client
|
||||
}
|
||||
|
||||
func New(baseURL, serviceToken string) *Client {
|
||||
return &Client{BaseURL: baseURL, ServiceToken: serviceToken, HTTPClient: &http.Client{Timeout: 10 * time.Second}}
|
||||
}
|
||||
|
||||
// VerifyTenant fragt Core, ob tenantSlug als Tenant gefuehrt wird. Transport-
|
||||
// und Serverfehler kommen als Fehler zurueck, nie als "gueltig": der Aufrufer
|
||||
// muss fail-closed reagieren.
|
||||
func (c *Client) VerifyTenant(ctx context.Context, tenantSlug string) (bool, error) {
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodGet, c.BaseURL+"/tenants/"+url.PathEscape(tenantSlug), nil)
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("tenantregistryclient: request bauen: %w", err)
|
||||
}
|
||||
req.Header.Set("X-Service-Token", c.ServiceToken)
|
||||
resp, err := c.HTTPClient.Do(req)
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("tenantregistryclient: aufruf fehlgeschlagen: %w", err)
|
||||
}
|
||||
defer func() { _ = resp.Body.Close() }()
|
||||
switch resp.StatusCode {
|
||||
case http.StatusOK:
|
||||
return true, nil
|
||||
case http.StatusNotFound:
|
||||
return false, nil
|
||||
default:
|
||||
return false, fmt.Errorf("tenantregistryclient: unerwarteter status %d", resp.StatusCode)
|
||||
}
|
||||
}
|
||||
@@ -13,7 +13,7 @@ import (
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"gitea.perlbach24.de/scripte/nexarch/dms/internal/httpclient"
|
||||
"gitea.perlbach24.de/scripte/nexarch/common/httpclient"
|
||||
"gitea.perlbach24.de/scripte/nexarch/dms/internal/jobqueue"
|
||||
)
|
||||
|
||||
@@ -21,8 +21,8 @@ const (
|
||||
ModuleName = "dms"
|
||||
ObjectType = "document"
|
||||
|
||||
TriggerCreated = "document.created"
|
||||
TriggerStatusChanged = "document.status_changed"
|
||||
TriggerCreated = "document.created"
|
||||
TriggerStatusChanged = "document.status_changed"
|
||||
TriggerDeadlineReached = "document.deadline_reached"
|
||||
|
||||
JobType = "workflow_event_publish"
|
||||
|
||||
@@ -23,11 +23,11 @@ import (
|
||||
// bildet dieser Fake den echten, aus dem Quellcode gelesenen HTTP-Vertrag
|
||||
// nach — dokumentiert im PRUEFPROTOKOLL als Grenze dieses Tests.
|
||||
type fakeWorkflow struct {
|
||||
mu sync.Mutex
|
||||
token string
|
||||
registrations []registerRequest
|
||||
events []publishEventRequest
|
||||
unreachable bool
|
||||
mu sync.Mutex
|
||||
token string
|
||||
registrations []registerRequest
|
||||
events []publishEventRequest
|
||||
unreachable bool
|
||||
rejectAllEvents bool
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user