Skip to content

Commit d448ea7

Browse files
grokifyclaude
andcommitted
test: add unit tests for core library
Add comprehensive tests for: - pidl_test.go: Flow methods, Protocol lookups - parse_test.go: JSON parsing, round-trip serialization - validate_test.go: Validation rules and error reporting - operations_test.go: File operations, SanitizeID, TitleCase 60 tests total across all packages. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
1 parent 8bf725d commit d448ea7

4 files changed

Lines changed: 791 additions & 0 deletions

File tree

operations_test.go

Lines changed: 222 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,222 @@
1+
package pidl
2+
3+
import (
4+
"os"
5+
"path/filepath"
6+
"testing"
7+
)
8+
9+
func TestValidateFile(t *testing.T) {
10+
// Create a temp file with valid content
11+
dir := t.TempDir()
12+
filename := filepath.Join(dir, "test.json")
13+
14+
p := NewMinimalProtocol("test", "Test Protocol")
15+
if err := WriteProtocolFile(filename, p); err != nil {
16+
t.Fatalf("WriteProtocolFile() error = %v", err)
17+
}
18+
19+
// Validate it
20+
parsed, errs, err := ValidateFile(filename)
21+
if err != nil {
22+
t.Fatalf("ValidateFile() error = %v", err)
23+
}
24+
if errs.HasErrors() {
25+
t.Errorf("ValidateFile() validation errors = %v", errs)
26+
}
27+
if parsed.ProtocolMeta.ID != "test" {
28+
t.Errorf("Protocol ID = %q, want %q", parsed.ProtocolMeta.ID, "test")
29+
}
30+
}
31+
32+
func TestValidateFileNotFound(t *testing.T) {
33+
_, _, err := ValidateFile("/nonexistent/file.json")
34+
if err == nil {
35+
t.Error("ValidateFile() should error on missing file")
36+
}
37+
}
38+
39+
func TestValidateFileInvalidJSON(t *testing.T) {
40+
dir := t.TempDir()
41+
filename := filepath.Join(dir, "invalid.json")
42+
43+
if err := os.WriteFile(filename, []byte("not json"), 0644); err != nil {
44+
t.Fatal(err)
45+
}
46+
47+
_, _, err := ValidateFile(filename)
48+
if err == nil {
49+
t.Error("ValidateFile() should error on invalid JSON")
50+
}
51+
}
52+
53+
func TestValidateFiles(t *testing.T) {
54+
dir := t.TempDir()
55+
56+
// Create valid file
57+
valid := filepath.Join(dir, "valid.json")
58+
p := NewMinimalProtocol("valid", "Valid")
59+
if err := WriteProtocolFile(valid, p); err != nil {
60+
t.Fatal(err)
61+
}
62+
63+
// Create invalid file
64+
invalid := filepath.Join(dir, "invalid.json")
65+
if err := os.WriteFile(invalid, []byte("{}"), 0644); err != nil {
66+
t.Fatal(err)
67+
}
68+
69+
results := ValidateFiles([]string{valid, invalid})
70+
71+
if len(results) != 2 {
72+
t.Fatalf("ValidateFiles() returned %d results, want 2", len(results))
73+
}
74+
75+
if !results[0].IsValid() {
76+
t.Errorf("results[0] should be valid")
77+
}
78+
79+
if results[1].IsValid() {
80+
t.Errorf("results[1] should be invalid")
81+
}
82+
}
83+
84+
func TestFileValidationResultIsValid(t *testing.T) {
85+
tests := []struct {
86+
name string
87+
result FileValidationResult
88+
want bool
89+
}{
90+
{
91+
name: "valid",
92+
result: FileValidationResult{},
93+
want: true,
94+
},
95+
{
96+
name: "parse error",
97+
result: FileValidationResult{
98+
ParseErr: os.ErrNotExist,
99+
},
100+
want: false,
101+
},
102+
{
103+
name: "validation errors",
104+
result: FileValidationResult{
105+
Errors: ValidationErrors{{Field: "x", Message: "y"}},
106+
},
107+
want: false,
108+
},
109+
}
110+
111+
for _, tt := range tests {
112+
t.Run(tt.name, func(t *testing.T) {
113+
if got := tt.result.IsValid(); got != tt.want {
114+
t.Errorf("IsValid() = %v, want %v", got, tt.want)
115+
}
116+
})
117+
}
118+
}
119+
120+
func TestNewProtocol(t *testing.T) {
121+
p := NewProtocol("test-id", "Test Name")
122+
123+
if p.ProtocolMeta.ID != "test-id" {
124+
t.Errorf("ID = %q, want %q", p.ProtocolMeta.ID, "test-id")
125+
}
126+
if p.ProtocolMeta.Name != "Test Name" {
127+
t.Errorf("Name = %q, want %q", p.ProtocolMeta.Name, "Test Name")
128+
}
129+
}
130+
131+
func TestNewMinimalProtocol(t *testing.T) {
132+
p := NewMinimalProtocol("test", "Test")
133+
134+
if !p.IsValid() {
135+
errs := p.Validate()
136+
t.Errorf("NewMinimalProtocol() should be valid, got errors: %v", errs)
137+
}
138+
139+
if len(p.Entities) < 2 {
140+
t.Errorf("NewMinimalProtocol() should have at least 2 entities")
141+
}
142+
if len(p.Flows) < 1 {
143+
t.Errorf("NewMinimalProtocol() should have at least 1 flow")
144+
}
145+
}
146+
147+
func TestWriteProtocolFile(t *testing.T) {
148+
dir := t.TempDir()
149+
filename := filepath.Join(dir, "subdir", "test.json")
150+
151+
p := NewMinimalProtocol("test", "Test")
152+
if err := WriteProtocolFile(filename, p); err != nil {
153+
t.Fatalf("WriteProtocolFile() error = %v", err)
154+
}
155+
156+
// Verify file exists
157+
if _, err := os.Stat(filename); err != nil {
158+
t.Errorf("File should exist: %v", err)
159+
}
160+
161+
// Read it back
162+
p2, err := ParseFile(filename)
163+
if err != nil {
164+
t.Fatalf("ParseFile() error = %v", err)
165+
}
166+
167+
if p2.ProtocolMeta.ID != p.ProtocolMeta.ID {
168+
t.Errorf("Round-trip ID = %q, want %q", p2.ProtocolMeta.ID, p.ProtocolMeta.ID)
169+
}
170+
}
171+
172+
func TestTitleCase(t *testing.T) {
173+
tests := []struct {
174+
input string
175+
want string
176+
}{
177+
{"hello world", "Hello World"},
178+
{"HELLO WORLD", "Hello World"},
179+
{"hello", "Hello"},
180+
{"", ""},
181+
{"my protocol", "My Protocol"},
182+
{"oauth 2 0", "Oauth 2 0"},
183+
}
184+
185+
for _, tt := range tests {
186+
t.Run(tt.input, func(t *testing.T) {
187+
got := TitleCase(tt.input)
188+
if got != tt.want {
189+
t.Errorf("TitleCase(%q) = %q, want %q", tt.input, got, tt.want)
190+
}
191+
})
192+
}
193+
}
194+
195+
func TestSanitizeID(t *testing.T) {
196+
tests := []struct {
197+
input string
198+
want string
199+
}{
200+
{"test", "test"},
201+
{"Test", "test"},
202+
{"TEST", "test"},
203+
{"test-id", "test-id"},
204+
{"test_id", "test_id"},
205+
{"Test ID", "test_id"},
206+
{"My Protocol", "my_protocol"},
207+
{"123abc", "p123abc"},
208+
{"", "protocol"},
209+
{"---", "protocol"},
210+
{"OAuth 2.0", "oauth_2_0"},
211+
{"MCP Tool", "mcp_tool"},
212+
}
213+
214+
for _, tt := range tests {
215+
t.Run(tt.input, func(t *testing.T) {
216+
got := SanitizeID(tt.input)
217+
if got != tt.want {
218+
t.Errorf("SanitizeID(%q) = %q, want %q", tt.input, got, tt.want)
219+
}
220+
})
221+
}
222+
}

parse_test.go

Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
package pidl
2+
3+
import (
4+
"strings"
5+
"testing"
6+
)
7+
8+
func TestParse(t *testing.T) {
9+
json := `{
10+
"protocol": {"id": "test", "name": "Test Protocol"},
11+
"entities": [
12+
{"id": "client", "name": "Client", "type": "client"},
13+
{"id": "server", "name": "Server", "type": "server"}
14+
],
15+
"flows": [
16+
{"from": "client", "to": "server", "action": "request"}
17+
]
18+
}`
19+
20+
p, err := Parse([]byte(json))
21+
if err != nil {
22+
t.Fatalf("Parse() error = %v", err)
23+
}
24+
25+
if p.ProtocolMeta.ID != "test" {
26+
t.Errorf("Protocol.ID = %q, want %q", p.ProtocolMeta.ID, "test")
27+
}
28+
29+
if len(p.Entities) != 2 {
30+
t.Errorf("len(Entities) = %d, want 2", len(p.Entities))
31+
}
32+
33+
if len(p.Flows) != 1 {
34+
t.Errorf("len(Flows) = %d, want 1", len(p.Flows))
35+
}
36+
}
37+
38+
func TestParseInvalidJSON(t *testing.T) {
39+
_, err := Parse([]byte("not json"))
40+
if err == nil {
41+
t.Error("Parse() should error on invalid JSON")
42+
}
43+
}
44+
45+
func TestParseReader(t *testing.T) {
46+
json := `{
47+
"protocol": {"id": "test", "name": "Test"},
48+
"entities": [
49+
{"id": "a", "name": "A", "type": "client"},
50+
{"id": "b", "name": "B", "type": "server"}
51+
],
52+
"flows": [{"from": "a", "to": "b", "action": "x"}]
53+
}`
54+
55+
p, err := ParseReader(strings.NewReader(json))
56+
if err != nil {
57+
t.Fatalf("ParseReader() error = %v", err)
58+
}
59+
60+
if p.ProtocolMeta.ID != "test" {
61+
t.Errorf("Protocol.ID = %q, want %q", p.ProtocolMeta.ID, "test")
62+
}
63+
}
64+
65+
func TestProtocolToJSON(t *testing.T) {
66+
p := &Protocol{
67+
ProtocolMeta: ProtocolMeta{
68+
ID: "test",
69+
Name: "Test",
70+
},
71+
Entities: []Entity{
72+
{ID: "a", Name: "A", Type: EntityTypeClient},
73+
{ID: "b", Name: "B", Type: EntityTypeServer},
74+
},
75+
Flows: []Flow{
76+
{From: "a", To: "b", Action: "request"},
77+
},
78+
}
79+
80+
data, err := p.ToJSON()
81+
if err != nil {
82+
t.Fatalf("ToJSON() error = %v", err)
83+
}
84+
85+
// Parse it back
86+
p2, err := Parse(data)
87+
if err != nil {
88+
t.Fatalf("Parse(ToJSON()) error = %v", err)
89+
}
90+
91+
if p2.ProtocolMeta.ID != p.ProtocolMeta.ID {
92+
t.Errorf("Round-trip ID = %q, want %q", p2.ProtocolMeta.ID, p.ProtocolMeta.ID)
93+
}
94+
}
95+
96+
func TestMustParse(t *testing.T) {
97+
json := `{
98+
"protocol": {"id": "test", "name": "Test"},
99+
"entities": [
100+
{"id": "a", "name": "A", "type": "client"},
101+
{"id": "b", "name": "B", "type": "server"}
102+
],
103+
"flows": [{"from": "a", "to": "b", "action": "x"}]
104+
}`
105+
106+
// Should not panic
107+
p := MustParse([]byte(json))
108+
if p.ProtocolMeta.ID != "test" {
109+
t.Errorf("MustParse ID = %q, want %q", p.ProtocolMeta.ID, "test")
110+
}
111+
}
112+
113+
func TestMustParsePanics(t *testing.T) {
114+
defer func() {
115+
if r := recover(); r == nil {
116+
t.Error("MustParse should panic on invalid JSON")
117+
}
118+
}()
119+
MustParse([]byte("invalid"))
120+
}

0 commit comments

Comments
 (0)