Files
RenewCMS/test/activity_log_backend_qa_test.go

174 lines
6.3 KiB
Go

package test
import (
"net/http"
"net/http/httptest"
"os"
"path/filepath"
"regexp"
"strconv"
"strings"
"testing"
)
func TestActivityLogBackendQAResetResponseIsIndistinguishable(t *testing.T) {
t.Run("unknown email answers the same bytes as an existing account", func(t *testing.T) {
e := newActivityHTTPEnv(t, 1000)
known := e.send(http.MethodPost, "/v1/auth/password-reset", `{"email":"marta@example.com"}`)
unknown := e.send(http.MethodPost, "/v1/auth/password-reset", `{"email":"nobody@example.com"}`)
if known.Code != unknown.Code || known.Body.String() != unknown.Body.String() {
t.Errorf("responses differ: %d %q versus %d %q", known.Code, known.Body.String(), unknown.Code, unknown.Body.String())
}
})
}
func TestActivityLogBackendQAProducersSurviveRecorderFailure(t *testing.T) {
type producer func(e *activityHTTPEnv) *httptest.ResponseRecorder
producers := map[string]producer{
"login failure": func(e *activityHTTPEnv) *httptest.ResponseRecorder {
return e.send(http.MethodPost, "/v1/auth/login", `{"username":"marta","password":"wrongpassword1"}`)
},
"logout": func(e *activityHTTPEnv) *httptest.ResponseRecorder {
return e.send(http.MethodPost, "/v1/auth/logout", "", asUser(e, activityOwnerID))
},
"password reset request": func(e *activityHTTPEnv) *httptest.ResponseRecorder {
return e.send(http.MethodPost, "/v1/auth/password-reset", `{"email":"marta@example.com"}`)
},
"api key creation": func(e *activityHTTPEnv) *httptest.ResponseRecorder {
return e.send(http.MethodPost, "/v1/api-key", `{"name":"Deploy bot"}`, asUser(e, activityOwnerID))
},
"api key revoke": func(e *activityHTTPEnv) *httptest.ResponseRecorder {
_, id := e.issueKey(t, activityOwnerID, "Deploy bot")
return e.send(http.MethodDelete, "/v1/api-key/"+strconv.Itoa(int(id)), "", asUser(e, activityOwnerID))
},
"retention change": func(e *activityHTTPEnv) *httptest.ResponseRecorder {
return e.send(http.MethodPatch, "/v1/settings", `{"activity_retention_days": 30}`, asUser(e, activityOwnerID))
},
}
scrub := regexp.MustCompile(`"(id|key|prefix|created_at)":\s*("[^"]*"|[0-9]+)`)
for name, run := range producers {
t.Run(name, func(t *testing.T) {
healthy := run(newActivityHTTPEnv(t, 1000))
broken := newActivityHTTPEnv(t, 1000)
broken.probe.appendErr = errDiskFull
got := run(broken)
if got.Code != healthy.Code || scrub.ReplaceAllString(got.Body.String(), "") != scrub.ReplaceAllString(healthy.Body.String(), "") {
t.Errorf("a failing recorder changed the answer: %d %q versus %d %q", got.Code, got.Body.String(), healthy.Code, healthy.Body.String())
}
})
}
}
func TestActivityLogBackendQAGuards(t *testing.T) {
read := func(t *testing.T, root string, suffix string) map[string]string {
t.Helper()
files := map[string]string{}
err := filepath.Walk(filepath.Join("..", root), func(path string, info os.FileInfo, err error) error {
if err != nil || info.IsDir() || !strings.HasSuffix(path, suffix) {
return err
}
content, readErr := os.ReadFile(path)
files[path] = string(content)
return readErr
})
if err != nil {
t.Fatal(err)
}
if len(files) == 0 {
t.Fatalf("no files under %s", root)
}
return files
}
t.Run("The audit repository source uses no dialect specific SQL", func(t *testing.T) {
for path, source := range read(t, "internal/infrastructure/persistence/repositories", ".go") {
if !strings.Contains(filepath.Base(path), "audit") {
continue
}
for _, banned := range []string{"ILIKE", "strftime", "date_trunc"} {
if strings.Contains(source, banned) {
t.Errorf("%s contains %s", path, banned)
}
}
}
})
t.Run("The audit domain imports only the stdlib and domain packages", func(t *testing.T) {
importLine := regexp.MustCompile(`(?m)^\s*"([^"]+)"$`)
for path, source := range read(t, "internal/domain/audit", ".go") {
for _, match := range importLine.FindAllStringSubmatch(source, -1) {
if strings.Contains(match[1], ".") && !strings.HasPrefix(match[1], "RenewCMS/internal/domain/") {
t.Errorf("%s imports %s", path, match[1])
}
if strings.HasPrefix(match[1], "RenewCMS/") && !strings.HasPrefix(match[1], "RenewCMS/internal/domain/") {
t.Errorf("%s imports %s", path, match[1])
}
}
}
})
t.Run("The activity use cases and handlers keep their layering", func(t *testing.T) {
banned := []string{"gorm.io", "infrastructure/persistence", "infrastructure/mailer", "net/http", "go-chi", "RenewCMS/api"}
for path, source := range read(t, "internal/infrastructure/useCases", ".go") {
base := filepath.Base(path)
if !strings.Contains(base, "Activity") && !strings.Contains(base, "activity") {
continue
}
for _, word := range banned {
if strings.Contains(source, word+`"`) || strings.Contains(source, `"`+word) {
t.Errorf("%s imports %s", path, word)
}
}
}
for path, source := range read(t, "api/handlers/activity", ".go") {
for _, word := range []string{"gorm.io", "persistence", "infrastructure/mailer"} {
if strings.Contains(source, word) {
t.Errorf("%s references %s", path, word)
}
}
}
})
t.Run("The persistence repository asserts the audit contract", func(t *testing.T) {
for path, source := range read(t, "internal/infrastructure/persistence/repositories", "auditEventRepository.go") {
if !strings.Contains(source, "var _ audit.Repository = &") {
t.Errorf("%s lacks the interface assertion", path)
}
}
})
t.Run("The new Go files hold no comment", func(t *testing.T) {
line := regexp.MustCompile(`(?m)^\s*//`)
for _, root := range []string{"internal/domain/audit", "internal/infrastructure/auditing", "api/handlers/activity"} {
for path, source := range read(t, root, ".go") {
if line.MatchString(source) {
t.Errorf("%s holds a comment", path)
}
}
}
})
t.Run("The docs describe the activity surface", func(t *testing.T) {
wants := map[string][]string{
"docs/api.md": {"/v1/activity", "/v1/activity/summary", "/v1/activity/export", "activity_retention_days"},
"docs/architecture.md": {"audit"},
"docs/configuration.md": {"retention"},
}
for doc, needles := range wants {
content, err := os.ReadFile(filepath.Join("..", doc))
if err != nil {
t.Fatal(err)
}
for _, needle := range needles {
if !strings.Contains(string(content), needle) {
t.Errorf("%s does not mention %s", doc, needle)
}
}
}
})
}