validator_test.go

  1// SPDX-FileCopyrightText: Amolith <amolith@secluded.site>
  2//
  3// SPDX-License-Identifier: AGPL-3.0-or-later
  4
  5package mcp
  6
  7import (
  8	"strings"
  9	"testing"
 10
 11	"git.secluded.site/planning-mcp-server/internal/config"
 12)
 13
 14func TestPlanningValidator_ValidateSetGoalRequest(t *testing.T) {
 15	cfg := &config.Config{
 16		Planning: config.PlanningConfig{
 17			MaxGoalLength: 100,
 18		},
 19	}
 20	validator := NewPlanningValidator(cfg)
 21
 22	tests := []struct {
 23		name    string
 24		req     SetGoalRequest
 25		wantErr bool
 26		errMsg  string
 27	}{
 28		{
 29			name: "valid request",
 30			req: SetGoalRequest{
 31				Title:       "Test Goal",
 32				Description: "Valid description",
 33			},
 34			wantErr: false,
 35		},
 36		{
 37			name: "empty title",
 38			req: SetGoalRequest{
 39				Title:       "",
 40				Description: "Valid description",
 41			},
 42			wantErr: true,
 43			errMsg:  "title is required",
 44		},
 45		{
 46			name: "empty description",
 47			req: SetGoalRequest{
 48				Title:       "Valid title",
 49				Description: "",
 50			},
 51			wantErr: true,
 52			errMsg:  "description is required",
 53		},
 54		{
 55			name: "title too long",
 56			req: SetGoalRequest{
 57				Title:       strings.Repeat("x", 101),
 58				Description: "Valid description",
 59			},
 60			wantErr: true,
 61			errMsg:  "title too long",
 62		},
 63		{
 64			name: "description too long",
 65			req: SetGoalRequest{
 66				Title:       "Valid title",
 67				Description: strings.Repeat("x", 101),
 68			},
 69			wantErr: true,
 70			errMsg:  "description too long",
 71		},
 72		{
 73			name: "title at max length",
 74			req: SetGoalRequest{
 75				Title:       strings.Repeat("x", 100),
 76				Description: "Valid description",
 77			},
 78			wantErr: false,
 79		},
 80	}
 81
 82	for _, tt := range tests {
 83		t.Run(tt.name, func(t *testing.T) {
 84			err := validator.ValidateSetGoalRequest(tt.req)
 85			if tt.wantErr {
 86				if err == nil {
 87					t.Errorf("ValidateSetGoalRequest() expected error, got nil")
 88					return
 89				}
 90				if !strings.Contains(err.Error(), tt.errMsg) {
 91					t.Errorf("ValidateSetGoalRequest() error = %v, want error containing %v", err, tt.errMsg)
 92				}
 93			} else {
 94				if err != nil {
 95					t.Errorf("ValidateSetGoalRequest() error = %v, want nil", err)
 96				}
 97			}
 98		})
 99	}
100}
101
102func TestPlanningValidator_ValidateChangeGoalRequest(t *testing.T) {
103	cfg := &config.Config{
104		Planning: config.PlanningConfig{
105			MaxGoalLength: 50,
106		},
107	}
108	validator := NewPlanningValidator(cfg)
109
110	tests := []struct {
111		name    string
112		req     ChangeGoalRequest
113		wantErr bool
114		errMsg  string
115	}{
116		{
117			name: "valid request",
118			req: ChangeGoalRequest{
119				Title:       "New Goal",
120				Description: "New description",
121				Reason:      "Valid reason",
122			},
123			wantErr: false,
124		},
125		{
126			name: "empty title",
127			req: ChangeGoalRequest{
128				Title:       "",
129				Description: "Valid description",
130				Reason:      "Valid reason",
131			},
132			wantErr: true,
133			errMsg:  "title is required",
134		},
135		{
136			name: "empty description",
137			req: ChangeGoalRequest{
138				Title:       "Valid title",
139				Description: "",
140				Reason:      "Valid reason",
141			},
142			wantErr: true,
143			errMsg:  "description is required",
144		},
145		{
146			name: "empty reason",
147			req: ChangeGoalRequest{
148				Title:       "Valid title",
149				Description: "Valid description",
150				Reason:      "",
151			},
152			wantErr: true,
153			errMsg:  "reason is required",
154		},
155		{
156			name: "reason too long",
157			req: ChangeGoalRequest{
158				Title:       "Valid title",
159				Description: "Valid description",
160				Reason:      strings.Repeat("x", 51),
161			},
162			wantErr: true,
163			errMsg:  "reason too long",
164		},
165	}
166
167	for _, tt := range tests {
168		t.Run(tt.name, func(t *testing.T) {
169			err := validator.ValidateChangeGoalRequest(tt.req)
170			if tt.wantErr {
171				if err == nil {
172					t.Errorf("ValidateChangeGoalRequest() expected error, got nil")
173					return
174				}
175				if !strings.Contains(err.Error(), tt.errMsg) {
176					t.Errorf("ValidateChangeGoalRequest() error = %v, want error containing %v", err, tt.errMsg)
177				}
178			} else {
179				if err != nil {
180					t.Errorf("ValidateChangeGoalRequest() error = %v, want nil", err)
181				}
182			}
183		})
184	}
185}
186
187func TestPlanningValidator_ValidateAddTasksRequest(t *testing.T) {
188	cfg := &config.Config{
189		Planning: config.PlanningConfig{
190			MaxTaskLength: 50,
191		},
192	}
193	validator := NewPlanningValidator(cfg)
194
195	tests := []struct {
196		name    string
197		req     AddTasksRequest
198		wantErr bool
199		errMsg  string
200	}{
201		{
202			name: "valid single task",
203			req: AddTasksRequest{
204				Tasks: []MCPTaskInput{
205					{Title: "Valid task", Description: "Valid description"},
206				},
207			},
208			wantErr: false,
209		},
210		{
211			name: "valid multiple tasks",
212			req: AddTasksRequest{
213				Tasks: []MCPTaskInput{
214					{Title: "Task 1", Description: "Description 1"},
215					{Title: "Task 2", Description: ""},
216					{Title: "Task 3", Description: "Description 3"},
217				},
218			},
219			wantErr: false,
220		},
221		{
222			name: "empty tasks array",
223			req: AddTasksRequest{
224				Tasks: []MCPTaskInput{},
225			},
226			wantErr: true,
227			errMsg:  "at least one task is required",
228		},
229		{
230			name: "task with empty title",
231			req: AddTasksRequest{
232				Tasks: []MCPTaskInput{
233					{Title: "", Description: "Valid description"},
234				},
235			},
236			wantErr: true,
237			errMsg:  "task 0: title is required",
238		},
239		{
240			name: "task with title too long",
241			req: AddTasksRequest{
242				Tasks: []MCPTaskInput{
243					{Title: strings.Repeat("x", 51), Description: "Valid description"},
244				},
245			},
246			wantErr: true,
247			errMsg:  "task 0: title too long",
248		},
249		{
250			name: "task with description too long",
251			req: AddTasksRequest{
252				Tasks: []MCPTaskInput{
253					{Title: "Valid title", Description: strings.Repeat("x", 51)},
254				},
255			},
256			wantErr: true,
257			errMsg:  "task 0: description too long",
258		},
259		{
260			name: "second task invalid",
261			req: AddTasksRequest{
262				Tasks: []MCPTaskInput{
263					{Title: "Valid task", Description: "Valid description"},
264					{Title: "", Description: "Another description"},
265				},
266			},
267			wantErr: true,
268			errMsg:  "task 1: title is required",
269		},
270	}
271
272	for _, tt := range tests {
273		t.Run(tt.name, func(t *testing.T) {
274			err := validator.ValidateAddTasksRequest(tt.req)
275			if tt.wantErr {
276				if err == nil {
277					t.Errorf("ValidateAddTasksRequest() expected error, got nil")
278					return
279				}
280				if !strings.Contains(err.Error(), tt.errMsg) {
281					t.Errorf("ValidateAddTasksRequest() error = %v, want error containing %v", err, tt.errMsg)
282				}
283			} else {
284				if err != nil {
285					t.Errorf("ValidateAddTasksRequest() error = %v, want nil", err)
286				}
287			}
288		})
289	}
290}
291
292func TestPlanningValidator_ValidateGetTasksRequest(t *testing.T) {
293	cfg := &config.Config{
294		Planning: config.PlanningConfig{},
295	}
296	validator := NewPlanningValidator(cfg)
297
298	tests := []struct {
299		name    string
300		req     GetTasksRequest
301		wantErr bool
302		errMsg  string
303	}{
304		{
305			name:    "empty status (valid)",
306			req:     GetTasksRequest{Status: ""},
307			wantErr: false,
308		},
309		{
310			name:    "valid status all",
311			req:     GetTasksRequest{Status: "all"},
312			wantErr: false,
313		},
314		{
315			name:    "valid status pending",
316			req:     GetTasksRequest{Status: "pending"},
317			wantErr: false,
318		},
319		{
320			name:    "valid status in_progress",
321			req:     GetTasksRequest{Status: "in_progress"},
322			wantErr: false,
323		},
324		{
325			name:    "valid status completed",
326			req:     GetTasksRequest{Status: "completed"},
327			wantErr: false,
328		},
329		{
330			name:    "valid status cancelled",
331			req:     GetTasksRequest{Status: "cancelled"},
332			wantErr: false,
333		},
334		{
335			name:    "valid status failed",
336			req:     GetTasksRequest{Status: "failed"},
337			wantErr: false,
338		},
339		{
340			name:    "invalid status",
341			req:     GetTasksRequest{Status: "invalid"},
342			wantErr: true,
343			errMsg:  "invalid status 'invalid'",
344		},
345		{
346			name:    "invalid status case sensitive",
347			req:     GetTasksRequest{Status: "Pending"},
348			wantErr: true,
349			errMsg:  "invalid status 'Pending'",
350		},
351	}
352
353	for _, tt := range tests {
354		t.Run(tt.name, func(t *testing.T) {
355			err := validator.ValidateGetTasksRequest(tt.req)
356			if tt.wantErr {
357				if err == nil {
358					t.Errorf("ValidateGetTasksRequest() expected error, got nil")
359					return
360				}
361				if !strings.Contains(err.Error(), tt.errMsg) {
362					t.Errorf("ValidateGetTasksRequest() error = %v, want error containing %v", err, tt.errMsg)
363				}
364			} else {
365				if err != nil {
366					t.Errorf("ValidateGetTasksRequest() error = %v, want nil", err)
367				}
368			}
369		})
370	}
371}
372
373func TestPlanningValidator_ValidateUpdateTaskStatusesRequest(t *testing.T) {
374	cfg := &config.Config{
375		Planning: config.PlanningConfig{},
376	}
377	validator := NewPlanningValidator(cfg)
378
379	tests := []struct {
380		name    string
381		req     UpdateTaskStatusesRequest
382		wantErr bool
383		errMsg  string
384	}{
385		{
386			name: "valid single update",
387			req: UpdateTaskStatusesRequest{
388				Tasks: []MCPTaskUpdateInput{
389					{TaskID: "task1", Status: "completed"},
390				},
391			},
392			wantErr: false,
393		},
394		{
395			name: "valid multiple updates",
396			req: UpdateTaskStatusesRequest{
397				Tasks: []MCPTaskUpdateInput{
398					{TaskID: "task1", Status: "completed"},
399					{TaskID: "task2", Status: "in_progress"},
400					{TaskID: "task3", Status: "failed"},
401				},
402			},
403			wantErr: false,
404		},
405		{
406			name: "empty updates array",
407			req: UpdateTaskStatusesRequest{
408				Tasks: []MCPTaskUpdateInput{},
409			},
410			wantErr: true,
411			errMsg:  "at least one task update is required",
412		},
413		{
414			name: "empty task ID",
415			req: UpdateTaskStatusesRequest{
416				Tasks: []MCPTaskUpdateInput{
417					{TaskID: "", Status: "completed"},
418				},
419			},
420			wantErr: true,
421			errMsg:  "task update 0: task_id is required",
422		},
423		{
424			name: "empty status",
425			req: UpdateTaskStatusesRequest{
426				Tasks: []MCPTaskUpdateInput{
427					{TaskID: "task1", Status: ""},
428				},
429			},
430			wantErr: true,
431			errMsg:  "task update 0: status is required",
432		},
433		{
434			name: "invalid status",
435			req: UpdateTaskStatusesRequest{
436				Tasks: []MCPTaskUpdateInput{
437					{TaskID: "task1", Status: "invalid"},
438				},
439			},
440			wantErr: true,
441			errMsg:  "task update 0: invalid status 'invalid'",
442		},
443		{
444			name: "second update invalid",
445			req: UpdateTaskStatusesRequest{
446				Tasks: []MCPTaskUpdateInput{
447					{TaskID: "task1", Status: "completed"},
448					{TaskID: "", Status: "pending"},
449				},
450			},
451			wantErr: true,
452			errMsg:  "task update 1: task_id is required",
453		},
454	}
455
456	for _, tt := range tests {
457		t.Run(tt.name, func(t *testing.T) {
458			err := validator.ValidateUpdateTaskStatusesRequest(tt.req)
459			if tt.wantErr {
460				if err == nil {
461					t.Errorf("ValidateUpdateTaskStatusesRequest() expected error, got nil")
462					return
463				}
464				if !strings.Contains(err.Error(), tt.errMsg) {
465					t.Errorf("ValidateUpdateTaskStatusesRequest() error = %v, want error containing %v", err, tt.errMsg)
466				}
467			} else {
468				if err != nil {
469					t.Errorf("ValidateUpdateTaskStatusesRequest() error = %v, want nil", err)
470				}
471			}
472		})
473	}
474}
475
476func TestPlanningValidator_ValidateDeleteTasksRequest(t *testing.T) {
477	cfg := &config.Config{
478		Planning: config.PlanningConfig{},
479	}
480	validator := NewPlanningValidator(cfg)
481
482	tests := []struct {
483		name    string
484		req     DeleteTasksRequest
485		wantErr bool
486		errMsg  string
487	}{
488		{
489			name: "valid single task ID",
490			req: DeleteTasksRequest{
491				TaskIDs: []string{"task1"},
492			},
493			wantErr: false,
494		},
495		{
496			name: "valid multiple task IDs",
497			req: DeleteTasksRequest{
498				TaskIDs: []string{"task1", "task2", "task3"},
499			},
500			wantErr: false,
501		},
502		{
503			name: "empty task IDs array",
504			req: DeleteTasksRequest{
505				TaskIDs: []string{},
506			},
507			wantErr: true,
508			errMsg:  "at least one task ID is required",
509		},
510		{
511			name: "empty task ID",
512			req: DeleteTasksRequest{
513				TaskIDs: []string{""},
514			},
515			wantErr: true,
516			errMsg:  "task ID 0 is empty",
517		},
518		{
519			name: "second task ID empty",
520			req: DeleteTasksRequest{
521				TaskIDs: []string{"task1", ""},
522			},
523			wantErr: true,
524			errMsg:  "task ID 1 is empty",
525		},
526	}
527
528	for _, tt := range tests {
529		t.Run(tt.name, func(t *testing.T) {
530			err := validator.ValidateDeleteTasksRequest(tt.req)
531			if tt.wantErr {
532				if err == nil {
533					t.Errorf("ValidateDeleteTasksRequest() expected error, got nil")
534					return
535				}
536				if !strings.Contains(err.Error(), tt.errMsg) {
537					t.Errorf("ValidateDeleteTasksRequest() error = %v, want error containing %v", err, tt.errMsg)
538				}
539			} else {
540				if err != nil {
541					t.Errorf("ValidateDeleteTasksRequest() error = %v, want nil", err)
542				}
543			}
544		})
545	}
546}
547
548func TestNewPlanningValidator(t *testing.T) {
549	cfg := &config.Config{
550		Planning: config.PlanningConfig{
551			MaxGoalLength: 100,
552			MaxTaskLength: 200,
553		},
554	}
555
556	validator := NewPlanningValidator(cfg)
557
558	if validator == nil {
559		t.Error("NewPlanningValidator() returned nil")
560		return
561	}
562
563	if validator.config != cfg {
564		t.Error("NewPlanningValidator() did not set config correctly")
565	}
566}