package tentacle import ( "bufio" "bytes" "errors" "io" "strings" "testing" ) func TestFramingBoundsAndRequiresNewline(t *testing.T) { for _, testCase := range []struct { name string input string want error }{ {"valid", strings.Repeat("x", MaxFrameBytes) + "\n", nil}, {"oversized", strings.Repeat("x", MaxFrameBytes+1) + "\n", ErrLimit}, {"unterminated", `{}`, io.ErrUnexpectedEOF}, {"empty", "", io.EOF}, } { t.Run(testCase.name, func(t *testing.T) { _, err := read_frame(bufio.NewReaderSize(strings.NewReader(testCase.input), 32)) if !errors.Is(err, testCase.want) { t.Fatalf("got %v, want %v", err, testCase.want) } }) } } func TestResponseValidation(t *testing.T) { for _, valid := range []string{ `{"jsonrpc":"2.0","id":"1","result":null}`, `{"jsonrpc":"2.0","id":"1","result":{"text":"é猫😀é"}}`, `{"jsonrpc":"2.0","id":"1","error":{"code":-32601,"message":"Unknown method"}}`, } { if _, err := decode_response([]byte(valid)); err != nil { t.Errorf("valid response rejected: %v", err) } } for _, invalid := range []string{ `[]`, `null`, `{}`, `{"jsonrpc":"2.0","id":"1"}`, `{"jsonrpc":"2.0","id":"1","result":null,"error":null}`, `{"jsonrpc":"2.0","id":"1","error":null}`, `{"jsonrpc":"2.0","id":1,"result":null}`, `{"jsonrpc":"1.0","id":"1","result":null}`, `{"jsonrpc":"2.0","id":"1","id":"2","result":null}`, `{"jsonrpc":"2.0","id":"1","result":{"x":0,"x":1}}`, `{"jsonrpc":"2.0","id":"1","result":null,"extra":1}`, `{"jsonrpc":"2.0","id":"1","error":{"message":"missing code"}}`, `{"jsonrpc":"2.0","id":"1","error":{"code":null,"message":"null code"}}`, `{"jsonrpc":"2.0","id":"1","error":{"code":1,"message":null}}`, `{"jsonrpc":"2.0","id":"1","result":"` + string([]byte{0xff}) + `"}`, `{"jsonrpc":"2.0","id":"1","result":` + strings.Repeat("[", 40) + "0" + strings.Repeat("]", 40) + `}`, `{"jsonrpc":"2.0","id":"1","result":null} {}`, } { if _, err := decode_response([]byte(invalid)); err == nil { t.Errorf("invalid response accepted: %q", invalid) } } if strings.Contains((&RPCError{Code: 1, Message: "private text"}).Error(), "private") { t.Fatal("remote text leaked through Error") } } func FuzzResponseDecoder(f *testing.F) { f.Add([]byte(`{"jsonrpc":"2.0","id":"1","result":null}`)) f.Add([]byte(`{"jsonrpc":"2.0","id":"1","error":{"code":1,"message":"bad"}}`)) f.Fuzz(func(t *testing.T, data []byte) { if len(data) > MaxFrameBytes { t.Skip() } _, _ = decode_response(data) _, _ = read_frame(bufio.NewReader(bytes.NewReader(data))) }) } func TestSettingsRegistrationRequiresOneInteractiveCommand(t *testing.T) { command := Command{ID: "settings", Title: "Settings", Settings: true, Interaction: true, Presentation: true} registration := Registration{Protocol: Version{Major: 1}, Session: "test", Commands: []Command{command}} if err := validate_registration(registration, "test"); err != nil { t.Fatal(err) } registration.Commands[0].Interaction = false if err := validate_registration(registration, "test"); err == nil { t.Fatal("accepted a settings handler without interaction") } registration.Commands[0] = command command.ID = "other-settings" registration.Commands = append(registration.Commands, command) if err := validate_registration(registration, "test"); err == nil { t.Fatal("accepted two settings entry points") } }