Skip to content

Commit 3ddf779

Browse files
fzipiCopilot
andauthored
test: add tests for package level actions doc (#335)
* test: add tests for package level actions doc Signed-off-by: Felipe Zipitria <felipe.zipitria@owasp.org> * Update content/docs/seclang/actions.md Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * fix: remove empty line Signed-off-by: Felipe Zipitria <felipe.zipitria@owasp.org> --------- Signed-off-by: Felipe Zipitria <felipe.zipitria@owasp.org> Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
1 parent 9b4821d commit 3ddf779

File tree

4 files changed

+238
-2
lines changed

4 files changed

+238
-2
lines changed

content/docs/seclang/actions.md

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ title: "Actions"
33
description: "Actions available in Coraza"
44
lead: "The action of a rule defines how to handle HTTP requests that have matched one or more rule conditions."
55
date: 2020-10-06T08:48:57+00:00
6-
lastmod: "2026-01-15T14:26:37-03:00"
6+
lastmod: "2026-01-15T16:50:59-03:00"
77
draft: false
88
images: []
99
menu:
@@ -28,7 +28,6 @@ Disruptive actions will NOT be executed if the `SecRuleEngine` is set to `Detect
2828
* **Meta-data actions** - used to provide more information about rules. Examples include id, rev, severity and msg.
2929
* **Data actions** - Not really actions, these are mere containers that hold data used by other actions. For example, the status action holds the status that will be used for blocking (if it takes place).
3030

31-
3231
## allow
3332

3433
**Description**: Stops rule processing on a successful match and allows a transaction to be proceed.allow will affect the entire transaction.

tools/actionsgen/main.go

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ import (
2323

2424
type Page struct {
2525
LastModification string
26+
PackageDescription string
2627
Actions []Action
2728
}
2829

@@ -78,6 +79,7 @@ func main() {
7879

7980
page := Page{
8081
LastModification: time.Now().Format(time.RFC3339),
82+
PackageDescription: getPackageDescription(root),
8183
}
8284

8385
for _, file := range files {
@@ -217,3 +219,56 @@ func parseAction(name string, doc string) Action {
217219
}
218220
return d
219221
}
222+
223+
// getPackageDescription extracts package-level documentation from the actions package.
224+
// It looks for doc.go first, then falls back to actions.go or any other .go file.
225+
func getPackageDescription(pkgPath string) string {
226+
// Try to find doc.go first
227+
docFile := filepath.Join(pkgPath, "doc.go")
228+
if desc := extractPackageDoc(docFile); desc != "" {
229+
return desc
230+
}
231+
232+
// Fall back to actions.go
233+
actionsFile := filepath.Join(pkgPath, "actions.go")
234+
if desc := extractPackageDoc(actionsFile); desc != "" {
235+
return desc
236+
}
237+
238+
// Try any .go file in the package
239+
files, err := filepath.Glob(filepath.Join(pkgPath, "*.go"))
240+
if err != nil {
241+
return ""
242+
}
243+
244+
for _, file := range files {
245+
if strings.HasSuffix(file, "_test.go") {
246+
continue
247+
}
248+
if desc := extractPackageDoc(file); desc != "" {
249+
return desc
250+
}
251+
}
252+
253+
return ""
254+
}
255+
256+
// extractPackageDoc extracts package documentation from a specific Go file.
257+
func extractPackageDoc(filePath string) string {
258+
src, err := os.ReadFile(filePath)
259+
if err != nil {
260+
return ""
261+
}
262+
263+
fSet := token.NewFileSet()
264+
f, err := parser.ParseFile(fSet, filePath, src, parser.ParseComments)
265+
if err != nil {
266+
return ""
267+
}
268+
269+
if f.Doc != nil {
270+
return strings.TrimSpace(f.Doc.Text())
271+
}
272+
273+
return ""
274+
}

tools/actionsgen/main_test.go

Lines changed: 178 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -270,3 +270,181 @@ type NewActionFn struct{}
270270
})
271271
}
272272
}
273+
274+
func TestExtractPackageDoc(t *testing.T) {
275+
tmpDir := t.TempDir()
276+
277+
tests := map[string]struct {
278+
fileContent string
279+
expected string
280+
}{
281+
"package with doc comment": {
282+
fileContent: `// Package actions provides implementations for various ModSecurity actions.
283+
// This is a multi-line package comment.
284+
package actions`,
285+
expected: "Package actions provides implementations for various ModSecurity actions.\nThis is a multi-line package comment.",
286+
},
287+
"package without doc comment": {
288+
fileContent: `package actions
289+
290+
type SomeFn struct{}`,
291+
expected: "",
292+
},
293+
"package with single line doc": {
294+
fileContent: `// Package actions provides action implementations.
295+
package actions`,
296+
expected: "Package actions provides action implementations.",
297+
},
298+
"package with whitespace in doc": {
299+
fileContent: `// Package actions provides action implementations.
300+
//
301+
// This package contains various actions.
302+
package actions`,
303+
expected: "Package actions provides action implementations.\n\nThis package contains various actions.",
304+
},
305+
}
306+
307+
for name, test := range tests {
308+
t.Run(name, func(t *testing.T) {
309+
testFile := filepath.Join(tmpDir, strings.ReplaceAll(t.Name(), "/", "_")+".go")
310+
err := os.WriteFile(testFile, []byte(test.fileContent), 0644)
311+
if err != nil {
312+
t.Fatalf("Failed to create test file: %v", err)
313+
}
314+
315+
result := extractPackageDoc(testFile)
316+
if result != test.expected {
317+
t.Errorf("want %q, got %q", test.expected, result)
318+
}
319+
})
320+
}
321+
}
322+
323+
func TestExtractPackageDocFileNotFound(t *testing.T) {
324+
result := extractPackageDoc("/nonexistent/file.go")
325+
if result != "" {
326+
t.Errorf("Expected empty string for non-existent file, got %q", result)
327+
}
328+
}
329+
330+
func TestGetPackageDescription(t *testing.T) {
331+
tmpDir := t.TempDir()
332+
333+
tests := map[string]struct {
334+
setup func(string) error
335+
expected string
336+
}{
337+
"finds doc.go first": {
338+
setup: func(dir string) error {
339+
docGo := `// Package actions from doc.go
340+
package actions`
341+
actionsGo := `// Package actions from actions.go
342+
package actions`
343+
otherGo := `// Package actions from other.go
344+
package actions`
345+
346+
if err := os.WriteFile(filepath.Join(dir, "doc.go"), []byte(docGo), 0644); err != nil {
347+
return err
348+
}
349+
if err := os.WriteFile(filepath.Join(dir, "actions.go"), []byte(actionsGo), 0644); err != nil {
350+
return err
351+
}
352+
return os.WriteFile(filepath.Join(dir, "other.go"), []byte(otherGo), 0644)
353+
},
354+
expected: "Package actions from doc.go",
355+
},
356+
"falls back to actions.go if no doc.go": {
357+
setup: func(dir string) error {
358+
actionsGo := `// Package actions from actions.go
359+
package actions`
360+
otherGo := `// Package actions from other.go
361+
package actions`
362+
363+
if err := os.WriteFile(filepath.Join(dir, "actions.go"), []byte(actionsGo), 0644); err != nil {
364+
return err
365+
}
366+
return os.WriteFile(filepath.Join(dir, "other.go"), []byte(otherGo), 0644)
367+
},
368+
expected: "Package actions from actions.go",
369+
},
370+
"finds any go file if no doc.go or actions.go": {
371+
setup: func(dir string) error {
372+
otherGo := `// Package actions from other.go
373+
package actions`
374+
return os.WriteFile(filepath.Join(dir, "other.go"), []byte(otherGo), 0644)
375+
},
376+
expected: "Package actions from other.go",
377+
},
378+
"skips test files": {
379+
setup: func(dir string) error {
380+
testGo := `// Package actions from test file
381+
package actions`
382+
otherGo := `// Package actions from regular file
383+
package actions`
384+
385+
if err := os.WriteFile(filepath.Join(dir, "actions_test.go"), []byte(testGo), 0644); err != nil {
386+
return err
387+
}
388+
return os.WriteFile(filepath.Join(dir, "other.go"), []byte(otherGo), 0644)
389+
},
390+
expected: "Package actions from regular file",
391+
},
392+
"returns empty string if no package doc found": {
393+
setup: func(dir string) error {
394+
noDocGo := `package actions
395+
396+
type SomeFn struct{}`
397+
return os.WriteFile(filepath.Join(dir, "nodoc.go"), []byte(noDocGo), 0644)
398+
},
399+
expected: "",
400+
},
401+
"returns empty string for empty directory": {
402+
setup: func(dir string) error {
403+
return nil
404+
},
405+
expected: "",
406+
},
407+
"handles files without package doc gracefully": {
408+
setup: func(dir string) error {
409+
file1 := `package actions
410+
411+
type SomeFn struct{}`
412+
file2 := `// Package actions has documentation
413+
package actions`
414+
415+
if err := os.WriteFile(filepath.Join(dir, "file1.go"), []byte(file1), 0644); err != nil {
416+
return err
417+
}
418+
return os.WriteFile(filepath.Join(dir, "file2.go"), []byte(file2), 0644)
419+
},
420+
expected: "Package actions has documentation",
421+
},
422+
}
423+
424+
for name, test := range tests {
425+
t.Run(name, func(t *testing.T) {
426+
testDir := filepath.Join(tmpDir, strings.ReplaceAll(t.Name(), "/", "_"))
427+
err := os.MkdirAll(testDir, 0755)
428+
if err != nil {
429+
t.Fatalf("Failed to create test directory: %v", err)
430+
}
431+
432+
err = test.setup(testDir)
433+
if err != nil {
434+
t.Fatalf("Setup failed: %v", err)
435+
}
436+
437+
result := getPackageDescription(testDir)
438+
if result != test.expected {
439+
t.Errorf("want %q, got %q", test.expected, result)
440+
}
441+
})
442+
}
443+
}
444+
445+
func TestGetPackageDescriptionNonExistentDir(t *testing.T) {
446+
result := getPackageDescription("/nonexistent/directory")
447+
if result != "" {
448+
t.Errorf("Expected empty string for non-existent directory, got %q", result)
449+
}
450+
}

tools/actionsgen/template.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,9 @@ toc: true
1515

1616
[//]: <> (This file is generated by tools/actionsgen. DO NOT EDIT.)
1717

18+
{{ if .PackageDescription -}}
19+
{{ .PackageDescription }}
20+
{{ else -}}
1821
Actions are defined as part of a `SecRule` or as parameter for `SecAction` or `SecDefaultAction`. A rule can have no or several actions which need to be separated by a comma.
1922

2023
Actions can be categorized by how they affect overall processing:
@@ -27,6 +30,7 @@ Disruptive actions will NOT be executed if the `SecRuleEngine` is set to `Detect
2730
* **Flow actions** - These actions affect the rule flow (for example skip or skipAfter).
2831
* **Meta-data actions** - used to provide more information about rules. Examples include id, rev, severity and msg.
2932
* **Data actions** - Not really actions, these are mere containers that hold data used by other actions. For example, the status action holds the status that will be used for blocking (if it takes place).
33+
{{ end -}}
3034

3135
{{ range .Actions }}
3236
## {{ .Name }}

0 commit comments

Comments
 (0)