CMS Go module renamed from git.dev.alexdunmow.com/block/ninja to git.dev.alexdunmow.com/block/cms (along with the git remote rename on gitea). All import paths, string-literal rules, test fixtures, and doc references updated; (historical 'ninja-orchestrator' refs preserved). go mod tidy regenerated checksums. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
456 lines
12 KiB
Go
456 lines
12 KiB
Go
package main
|
|
|
|
import (
|
|
"path/filepath"
|
|
"testing"
|
|
)
|
|
|
|
// ─── checkReinventedBackend ────────────────────────────────────────────────
|
|
|
|
func TestCheckReinventedBackend_TimestamptzDuplicate(t *testing.T) {
|
|
dir := t.TempDir()
|
|
writeFile(t, dir, "util.go", `package util
|
|
|
|
func timestamptz(t time.Time) pgtype.Timestamptz {
|
|
return pgtype.Timestamptz{Time: t, Valid: true}
|
|
}
|
|
`)
|
|
vs := checkReinventedBackend(dir)
|
|
found := false
|
|
for _, v := range vs {
|
|
if v.rule == "dup-timestamptz" {
|
|
found = true
|
|
}
|
|
}
|
|
if !found {
|
|
t.Fatalf("expected dup-timestamptz violation, got %#v", vs)
|
|
}
|
|
}
|
|
|
|
func TestCheckReinventedBackend_ToPgTimestamptzDuplicate(t *testing.T) {
|
|
dir := t.TempDir()
|
|
writeFile(t, dir, "util.go", `package util
|
|
|
|
func toPgTimestamptz(t time.Time) pgtype.Timestamptz {
|
|
return pgtype.Timestamptz{Time: t, Valid: true}
|
|
}
|
|
`)
|
|
vs := checkReinventedBackend(dir)
|
|
found := false
|
|
for _, v := range vs {
|
|
if v.rule == "dup-toPgTimestamptz" {
|
|
found = true
|
|
}
|
|
}
|
|
if !found {
|
|
t.Fatalf("expected dup-toPgTimestamptz violation, got %#v", vs)
|
|
}
|
|
}
|
|
|
|
func TestCheckReinventedBackend_TimestampFromTimeDuplicate(t *testing.T) {
|
|
dir := t.TempDir()
|
|
writeFile(t, dir, "util.go", `package util
|
|
|
|
func timestampFromTime(t time.Time) *timestamppb.Timestamp {
|
|
return timestamppb.New(t)
|
|
}
|
|
`)
|
|
vs := checkReinventedBackend(dir)
|
|
found := false
|
|
for _, v := range vs {
|
|
if v.rule == "dup-timestampFromTime" {
|
|
found = true
|
|
}
|
|
}
|
|
if !found {
|
|
t.Fatalf("expected dup-timestampFromTime violation, got %#v", vs)
|
|
}
|
|
}
|
|
|
|
func TestCheckReinventedBackend_TestFile_Skipped(t *testing.T) {
|
|
dir := t.TempDir()
|
|
writeFile(t, dir, "util_test.go", `package util
|
|
|
|
func timestamptz(t time.Time) pgtype.Timestamptz {
|
|
return pgtype.Timestamptz{Time: t, Valid: true}
|
|
}
|
|
`)
|
|
vs := checkReinventedBackend(dir)
|
|
for _, v := range vs {
|
|
if v.rule == "dup-timestamptz" {
|
|
t.Fatal("test files should be skipped for dup-timestamptz check")
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestCheckReinventedBackend_TypeAssertionWithHelpersImport_Flagged(t *testing.T) {
|
|
// A file that imports internal/helpers AND does m["key"].(string) should be flagged
|
|
dir := t.TempDir()
|
|
writeFile(t, dir, "service.go", `package service
|
|
|
|
import "git.dev.alexdunmow.com/block/cms/internal/helpers"
|
|
|
|
func render(m map[string]interface{}) string {
|
|
_ = helpers.GetStringOr(m, "key", "")
|
|
return m["title"].(string)
|
|
}
|
|
`)
|
|
vs := checkReinventedBackend(dir)
|
|
found := false
|
|
for _, v := range vs {
|
|
if v.rule == "use-helper-accessor" {
|
|
found = true
|
|
}
|
|
}
|
|
if !found {
|
|
t.Fatalf("expected use-helper-accessor violation for m[\"key\"].(string) with helpers imported, got %#v", vs)
|
|
}
|
|
}
|
|
|
|
func TestCheckReinventedBackend_TypeAssertionWithoutHelpersImport_NotFlagged(t *testing.T) {
|
|
// A file that does NOT import helpers should NOT be flagged for type assertions
|
|
dir := t.TempDir()
|
|
writeFile(t, dir, "service.go", `package service
|
|
|
|
func render(m map[string]interface{}) string {
|
|
return m["title"].(string)
|
|
}
|
|
`)
|
|
vs := checkReinventedBackend(dir)
|
|
for _, v := range vs {
|
|
if v.rule == "use-helper-accessor" {
|
|
t.Fatal("files without helpers import should NOT trigger use-helper-accessor")
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestCheckReinventedBackend_HelpersDirSelf_NotFlagged(t *testing.T) {
|
|
// The helpers package itself is allowed to define these functions
|
|
dir := t.TempDir()
|
|
helpersDir := filepath.Join(dir, "internal", "helpers")
|
|
writeTestFile(t, filepath.Join(helpersDir, "map.go"), `package helpers
|
|
|
|
import "git.dev.alexdunmow.com/block/cms/internal/helpers"
|
|
|
|
func GetStringOr(m map[string]interface{}, key, def string) string {
|
|
if v, ok := m[key]; ok {
|
|
return v.(string)
|
|
}
|
|
return def
|
|
}
|
|
`, 0644)
|
|
vs := checkReinventedBackend(dir)
|
|
for _, v := range vs {
|
|
if v.rule == "use-helper-accessor" {
|
|
t.Fatal("internal/helpers/ itself should be exempt from use-helper-accessor check")
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestCheckReinventedBackend_CleanFile_NoViolation(t *testing.T) {
|
|
dir := t.TempDir()
|
|
writeFile(t, dir, "service.go", `package service
|
|
|
|
func doWork() string {
|
|
return "hello"
|
|
}
|
|
`)
|
|
vs := checkReinventedBackend(dir)
|
|
if len(vs) != 0 {
|
|
t.Fatalf("expected no violations for clean Go file, got %d: %#v", len(vs), vs)
|
|
}
|
|
}
|
|
|
|
func TestCheckReinventedBackend_VendorDir_Skipped(t *testing.T) {
|
|
dir := t.TempDir()
|
|
vendorDir := filepath.Join(dir, "vendor", "somelib")
|
|
writeTestFile(t, filepath.Join(vendorDir, "util.go"), `package somelib
|
|
|
|
func timestamptz(t interface{}) interface{} {
|
|
return t
|
|
}
|
|
`, 0644)
|
|
vs := checkReinventedBackend(dir)
|
|
for _, v := range vs {
|
|
if v.rule == "dup-timestamptz" {
|
|
t.Fatal("vendor/ files should be skipped")
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestCheckReinventedBackend_BoolTypeAssertion_Flagged(t *testing.T) {
|
|
dir := t.TempDir()
|
|
writeFile(t, dir, "service.go", `package service
|
|
|
|
import "git.dev.alexdunmow.com/block/cms/internal/helpers"
|
|
|
|
func check(m map[string]interface{}) bool {
|
|
_ = helpers.GetBoolOr(m, "enabled", false)
|
|
return m["active"].(bool)
|
|
}
|
|
`)
|
|
vs := checkReinventedBackend(dir)
|
|
found := false
|
|
for _, v := range vs {
|
|
if v.rule == "use-helper-accessor" {
|
|
found = true
|
|
}
|
|
}
|
|
if !found {
|
|
t.Fatalf("expected use-helper-accessor for m[\"key\"].(bool) with helpers imported, got %#v", vs)
|
|
}
|
|
}
|
|
|
|
func TestCheckReinventedBackend_NonGoFile_Skipped(t *testing.T) {
|
|
dir := t.TempDir()
|
|
writeFile(t, dir, "util.ts", `// TypeScript: function timestamptz() {}
|
|
function timestamptz(t: Date) { return t }
|
|
`)
|
|
vs := checkReinventedBackend(dir)
|
|
if len(vs) != 0 {
|
|
t.Fatalf("expected no violations for .ts file (backend check only scans .go), got %d", len(vs))
|
|
}
|
|
}
|
|
|
|
// ─── checkReinventedFrontend ──────────────────────────────────────────────
|
|
|
|
func TestCheckReinventedFrontend_ClsxDirectly_Flagged(t *testing.T) {
|
|
dir := t.TempDir()
|
|
writeFile(t, dir, "comp.tsx", `import { clsx } from 'clsx'
|
|
|
|
export function Comp({ active }: { active: boolean }) {
|
|
return <div className={clsx('base', { active })} />
|
|
}
|
|
`)
|
|
vs := checkReinventedFrontend(dir)
|
|
found := false
|
|
for _, v := range vs {
|
|
if v.rule == "use-cn" {
|
|
found = true
|
|
}
|
|
}
|
|
if !found {
|
|
t.Fatalf("expected use-cn violation for clsx() usage, got %#v", vs)
|
|
}
|
|
}
|
|
|
|
func TestCheckReinventedFrontend_ClassnamesDirectly_Flagged(t *testing.T) {
|
|
dir := t.TempDir()
|
|
writeFile(t, dir, "comp.tsx", `import classnames from 'classnames'
|
|
|
|
export function Comp({ a }: { a: boolean }) {
|
|
return <div className={classnames('foo', { a })} />
|
|
}
|
|
`)
|
|
vs := checkReinventedFrontend(dir)
|
|
found := false
|
|
for _, v := range vs {
|
|
if v.rule == "use-cn" {
|
|
found = true
|
|
}
|
|
}
|
|
if !found {
|
|
t.Fatalf("expected use-cn violation for classnames() usage, got %#v", vs)
|
|
}
|
|
}
|
|
|
|
func TestCheckReinventedFrontend_CustomDebounce_Flagged(t *testing.T) {
|
|
dir := t.TempDir()
|
|
writeFile(t, dir, "utils-extra.ts", `// local debounce — do not commit
|
|
function debounce<T extends (...args: unknown[]) => void>(fn: T, delay: number): T {
|
|
let timer: ReturnType<typeof setTimeout>
|
|
return ((...args: unknown[]) => {
|
|
clearTimeout(timer)
|
|
timer = setTimeout(() => fn(...args), delay)
|
|
}) as T
|
|
}
|
|
`)
|
|
vs := checkReinventedFrontend(dir)
|
|
found := false
|
|
for _, v := range vs {
|
|
if v.rule == "use-debounce" {
|
|
found = true
|
|
}
|
|
}
|
|
if !found {
|
|
t.Fatalf("expected use-debounce violation for custom debounce function, got %#v", vs)
|
|
}
|
|
}
|
|
|
|
func TestCheckReinventedFrontend_CustomFormatBytes_Flagged(t *testing.T) {
|
|
dir := t.TempDir()
|
|
writeFile(t, dir, "format-extra.ts", `const formatBytes = (bytes: number): string => {
|
|
if (bytes < 1024) return bytes + ' B'
|
|
return (bytes / 1024).toFixed(1) + ' KB'
|
|
}
|
|
`)
|
|
vs := checkReinventedFrontend(dir)
|
|
found := false
|
|
for _, v := range vs {
|
|
if v.rule == "use-formatBytes" {
|
|
found = true
|
|
}
|
|
}
|
|
if !found {
|
|
t.Fatalf("expected use-formatBytes violation, got %#v", vs)
|
|
}
|
|
}
|
|
|
|
func TestCheckReinventedFrontend_CustomSlugify_Flagged(t *testing.T) {
|
|
dir := t.TempDir()
|
|
writeFile(t, dir, "helpers.ts", `const slugify = (s: string) => s.toLowerCase().replace(/\s+/g, '-')
|
|
`)
|
|
vs := checkReinventedFrontend(dir)
|
|
found := false
|
|
for _, v := range vs {
|
|
if v.rule == "use-slugify" {
|
|
found = true
|
|
}
|
|
}
|
|
if !found {
|
|
t.Fatalf("expected use-slugify violation, got %#v", vs)
|
|
}
|
|
}
|
|
|
|
func TestCheckReinventedFrontend_RawURLSearchParams_Flagged(t *testing.T) {
|
|
dir := t.TempDir()
|
|
writeFile(t, dir, "comp.tsx", `export function Comp() {
|
|
const params = new URLSearchParams(window.location.search)
|
|
return <div>{params.get('tab')}</div>
|
|
}
|
|
`)
|
|
vs := checkReinventedFrontend(dir)
|
|
found := false
|
|
for _, v := range vs {
|
|
if v.rule == "use-router-search" {
|
|
found = true
|
|
}
|
|
}
|
|
if !found {
|
|
t.Fatalf("expected use-router-search violation for URLSearchParams(window.location.search), got %#v", vs)
|
|
}
|
|
}
|
|
|
|
func TestCheckReinventedFrontend_AllowedFile_NotFlagged(t *testing.T) {
|
|
// lib/utils.ts is the canonical home — debounce there is OK
|
|
dir := t.TempDir()
|
|
utilsDir := filepath.Join(dir, "lib")
|
|
writeTestFile(t, filepath.Join(utilsDir, "utils.ts"), `// canonical utility definitions
|
|
const debounce = <T extends (...args: unknown[]) => void>(fn: T, delay: number): T => {
|
|
let timer: ReturnType<typeof setTimeout>
|
|
return ((...args: unknown[]) => {
|
|
clearTimeout(timer)
|
|
timer = setTimeout(() => fn(...args), delay)
|
|
}) as T
|
|
}
|
|
|
|
export { debounce }
|
|
`, 0644)
|
|
vs := checkReinventedFrontend(dir)
|
|
for _, v := range vs {
|
|
if v.rule == "use-debounce" {
|
|
t.Fatalf("lib/utils.ts should be exempt from use-debounce check, got violation at %s:%d", v.file, v.line)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestCheckReinventedFrontend_CommentLine_Skipped(t *testing.T) {
|
|
// Lines that are comments should be skipped
|
|
dir := t.TempDir()
|
|
writeFile(t, dir, "comp.tsx", `// You could use clsx() here but we use cn() instead
|
|
// const slugify = (s: string) => s.toLowerCase()
|
|
export const x = 1
|
|
`)
|
|
vs := checkReinventedFrontend(dir)
|
|
if len(vs) != 0 {
|
|
t.Fatalf("expected comment lines to be skipped, got %d violations: %#v", len(vs), vs)
|
|
}
|
|
}
|
|
|
|
func TestCheckReinventedFrontend_CleanFile_NoViolation(t *testing.T) {
|
|
dir := t.TempDir()
|
|
writeFile(t, dir, "comp.tsx", `import { cn } from '@/lib/utils'
|
|
|
|
export function Comp({ active }: { active: boolean }) {
|
|
return <div className={cn('base', { active })} />
|
|
}
|
|
`)
|
|
vs := checkReinventedFrontend(dir)
|
|
if len(vs) != 0 {
|
|
t.Fatalf("expected no violations for clean frontend file, got %d: %#v", len(vs), vs)
|
|
}
|
|
}
|
|
|
|
func TestCheckReinventedFrontend_NonTSFile_Skipped(t *testing.T) {
|
|
dir := t.TempDir()
|
|
// .go file — should not be scanned by frontend check
|
|
writeFile(t, dir, "service.go", `package service
|
|
|
|
const slugify = func(s string) string { return s }
|
|
`)
|
|
vs := checkReinventedFrontend(dir)
|
|
if len(vs) != 0 {
|
|
t.Fatalf("expected no violations for .go file in frontend scan, got %d", len(vs))
|
|
}
|
|
}
|
|
|
|
func TestCheckReinventedFrontend_NodeModules_Skipped(t *testing.T) {
|
|
dir := t.TempDir()
|
|
nmDir := filepath.Join(dir, "node_modules", "some-lib")
|
|
writeTestFile(t, filepath.Join(nmDir, "index.ts"), `function debounce() {}
|
|
`, 0644)
|
|
vs := checkReinventedFrontend(dir)
|
|
for _, v := range vs {
|
|
if v.rule == "use-debounce" {
|
|
t.Fatal("node_modules should be skipped")
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestCheckReinventedFrontend_RelativeFilePath(t *testing.T) {
|
|
dir := t.TempDir()
|
|
writeFile(t, dir, "utils-bad.ts", `function formatBytes(n: number) { return n + 'B' }
|
|
`)
|
|
vs := checkReinventedFrontend(dir)
|
|
if len(vs) == 0 {
|
|
t.Fatal("expected violation")
|
|
}
|
|
if vs[0].file != "utils-bad.ts" {
|
|
t.Fatalf("expected relative path 'utils-bad.ts', got %q", vs[0].file)
|
|
}
|
|
}
|
|
|
|
func TestCheckReinventedFrontend_CustomCountWords_Flagged(t *testing.T) {
|
|
dir := t.TempDir()
|
|
writeFile(t, dir, "seo-extra.ts", `const countWords = (text: string): number => text.split(/\s+/).filter(Boolean).length
|
|
`)
|
|
vs := checkReinventedFrontend(dir)
|
|
found := false
|
|
for _, v := range vs {
|
|
if v.rule == "use-countWords" {
|
|
found = true
|
|
}
|
|
}
|
|
if !found {
|
|
t.Fatalf("expected use-countWords violation, got %#v", vs)
|
|
}
|
|
}
|
|
|
|
func TestCheckReinventedFrontend_ProtoTimestampConversion_Flagged(t *testing.T) {
|
|
dir := t.TempDir()
|
|
writeFile(t, dir, "table.tsx", `export function formatTs(ts: { seconds: bigint }) {
|
|
return new Date(Number(ts.seconds) * 1000).toISOString()
|
|
}
|
|
`)
|
|
vs := checkReinventedFrontend(dir)
|
|
found := false
|
|
for _, v := range vs {
|
|
if v.rule == "use-shared-formatDate" {
|
|
found = true
|
|
}
|
|
}
|
|
if !found {
|
|
t.Fatalf("expected use-shared-formatDate violation for proto timestamp conversion, got %#v", vs)
|
|
}
|
|
}
|