mirror of
https://github.com/larksuite/cli.git
synced 2026-07-08 10:08:02 +08:00
* feat(contentsafety): add extension interface layer with Provider, Alert, and registry Change-Id: Ibeac6366c7201293057bc3b063f75ac34565bcd5 * feat(contentsafety): add normalize utility for JSON type conversion Change-Id: I7d4729a5ddcab2553abc110f8f6ecc88435ae921 * feat(contentsafety): add tree walker and regex scanner Change-Id: I215dad7cf3072711d05e45f7d384162e1f8752d4 * feat(contentsafety): add config loading with lazy creation, default rules, and allowlist matching Change-Id: I75e10df28f1f8d4f433cb2b469a0ff317af3bf70 * feat(contentsafety): add regex provider with config-driven scanning and allowlist Change-Id: I658889b3647cbbbde6881e0c5f7c13887a1eb1d4 * feat(contentsafety): add output core with mode parsing, path normalization, and scan orchestration Change-Id: I1cb9df75f1a4d176d660e2e7a9561314c3787191 * feat(contentsafety): add ScanForSafety entry point and Envelope alert field Change-Id: I5fdb311e1c8d983a35a58667970b9fd3ac729a5c * feat(contentsafety): integrate scanning into shortcut Out() and OutFormat() Change-Id: I33eef1dba14c8a9bd1998857311bdd611f33b916 * feat(contentsafety): integrate scanning into API/service output paths and register provider Change-Id: Ic3981db6c546a19eadea095d82175f92f4783bec * fix(contentsafety): emit stderr notice when lazy-creating default config Change-Id: Ia2491f7a17caceea3125ff9fb58d750dc196d7e7 * style: gofmt factory_default and exitcode Change-Id: I86c5afdfbbdb68d8137f0ca09ef3b5a1139f4b4e * fix(contentsafety): vfs for config I/O, mutex for lazy-create, sort matched rules, emit warn on --output path Change-Id: Ib4982cd54e1bfe0580a0eb03368e6ca818304e1b * fix(contentsafety): isolate scan goroutine errOut to prevent race on timeout Change-Id: Ia5a770d7387ba6d3b7fa318fc5f1384214ea10b7 * fix(contentsafety): deep-normalize typed slices so scanner can walk shortcut data Change-Id: I641e89113d1a2f2285ac6109bd3d7264f5845ea7 * fix(contentsafety): file perms 0600/0700, no result mutation, timeout test, scanTimeout comment Change-Id: Ie45a2e365ee7098e214e94f8871026cc12029d83
125 lines
3.6 KiB
Go
125 lines
3.6 KiB
Go
// Copyright (c) 2026 Lark Technologies Pte. Ltd.
|
|
// SPDX-License-Identifier: MIT
|
|
|
|
package contentsafety
|
|
|
|
import (
|
|
"io"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
func TestLoadConfig_ValidFile(t *testing.T) {
|
|
dir := t.TempDir()
|
|
content := `{
|
|
"allowlist": ["im", "drive.upload"],
|
|
"rules": [{"id": "r1", "pattern": "(?i)test_pattern"}]
|
|
}`
|
|
if err := os.WriteFile(filepath.Join(dir, "content-safety.json"), []byte(content), 0644); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
cfg, err := LoadConfig(dir)
|
|
if err != nil {
|
|
t.Fatalf("LoadConfig() error = %v", err)
|
|
}
|
|
if len(cfg.Allowlist) != 2 || cfg.Allowlist[0] != "im" {
|
|
t.Errorf("Allowlist = %v, want [im, drive.upload]", cfg.Allowlist)
|
|
}
|
|
if len(cfg.Rules) != 1 || cfg.Rules[0].ID != "r1" {
|
|
t.Fatalf("Rules = %v, want [{r1, ...}]", cfg.Rules)
|
|
}
|
|
if !cfg.Rules[0].Pattern.MatchString("TEST_PATTERN here") {
|
|
t.Error("compiled pattern should match")
|
|
}
|
|
}
|
|
|
|
func TestLoadConfig_InvalidJSON(t *testing.T) {
|
|
dir := t.TempDir()
|
|
os.WriteFile(filepath.Join(dir, "content-safety.json"), []byte(`{bad`), 0644)
|
|
_, err := LoadConfig(dir)
|
|
if err == nil {
|
|
t.Fatal("expected error for invalid JSON")
|
|
}
|
|
}
|
|
|
|
func TestLoadConfig_InvalidRegex(t *testing.T) {
|
|
dir := t.TempDir()
|
|
os.WriteFile(filepath.Join(dir, "content-safety.json"), []byte(`{"allowlist":[],"rules":[{"id":"bad","pattern":"(?P<broken"}]}`), 0644)
|
|
_, err := LoadConfig(dir)
|
|
if err == nil {
|
|
t.Fatal("expected error for invalid regex")
|
|
}
|
|
}
|
|
|
|
func TestLoadConfig_EmptyRules(t *testing.T) {
|
|
dir := t.TempDir()
|
|
os.WriteFile(filepath.Join(dir, "content-safety.json"), []byte(`{"allowlist":["all"],"rules":[]}`), 0644)
|
|
cfg, err := LoadConfig(dir)
|
|
if err != nil {
|
|
t.Fatalf("LoadConfig() error = %v", err)
|
|
}
|
|
if len(cfg.Rules) != 0 {
|
|
t.Errorf("Rules length = %d, want 0", len(cfg.Rules))
|
|
}
|
|
}
|
|
|
|
func TestEnsureDefaultConfig_CreatesFile(t *testing.T) {
|
|
dir := t.TempDir()
|
|
var buf strings.Builder
|
|
if err := EnsureDefaultConfig(dir, &buf); err != nil {
|
|
t.Fatalf("EnsureDefaultConfig() error = %v", err)
|
|
}
|
|
cfg, err := LoadConfig(dir)
|
|
if err != nil {
|
|
t.Fatalf("default config not loadable: %v", err)
|
|
}
|
|
if len(cfg.Rules) != 4 {
|
|
t.Errorf("default rules = %d, want 4", len(cfg.Rules))
|
|
}
|
|
if len(cfg.Allowlist) != 1 || cfg.Allowlist[0] != "all" {
|
|
t.Errorf("default allowlist = %v, want [all]", cfg.Allowlist)
|
|
}
|
|
if !strings.Contains(buf.String(), "notice: created default content-safety config") {
|
|
t.Errorf("expected stderr notice, got %q", buf.String())
|
|
}
|
|
}
|
|
|
|
func TestEnsureDefaultConfig_NoOverwrite(t *testing.T) {
|
|
dir := t.TempDir()
|
|
custom := `{"allowlist":[],"rules":[]}`
|
|
os.WriteFile(filepath.Join(dir, "content-safety.json"), []byte(custom), 0644)
|
|
EnsureDefaultConfig(dir, io.Discard)
|
|
data, _ := os.ReadFile(filepath.Join(dir, "content-safety.json"))
|
|
if string(data) != custom {
|
|
t.Error("should not overwrite existing file")
|
|
}
|
|
}
|
|
|
|
func TestIsAllowlisted(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
cmdPath string
|
|
list []string
|
|
want bool
|
|
}{
|
|
{"empty_list", "im.messages_search", nil, false},
|
|
{"all", "anything", []string{"all"}, true},
|
|
{"ALL_upper", "anything", []string{"ALL"}, true},
|
|
{"exact", "im.messages_search", []string{"im.messages_search"}, true},
|
|
{"prefix", "im.messages_search", []string{"im"}, true},
|
|
{"no_match", "drive.upload", []string{"im"}, false},
|
|
{"prefix_boundary", "im_extra", []string{"im"}, false},
|
|
{"multi", "drive.upload", []string{"im", "drive"}, true},
|
|
}
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
got := IsAllowlisted(tt.cmdPath, tt.list)
|
|
if got != tt.want {
|
|
t.Errorf("IsAllowlisted(%q, %v) = %v, want %v", tt.cmdPath, tt.list, got, tt.want)
|
|
}
|
|
})
|
|
}
|
|
}
|