kanboard-api/jsonrpc_test.go

392 lines
9.5 KiB
Go
Raw Normal View History

package kanboard
import (
"context"
"encoding/json"
"net/http"
"net/http/httptest"
"sync"
"testing"
)
func TestJSONRPCRequest_Marshal(t *testing.T) {
req := JSONRPCRequest{
JSONRPC: "2.0",
Method: "getTask",
ID: 1,
Params: map[string]int{"task_id": 42},
}
data, err := json.Marshal(req)
if err != nil {
t.Fatalf("failed to marshal request: %v", err)
}
var unmarshaled map[string]interface{}
if err := json.Unmarshal(data, &unmarshaled); err != nil {
t.Fatalf("failed to unmarshal request: %v", err)
}
if unmarshaled["jsonrpc"] != "2.0" {
t.Errorf("expected jsonrpc=2.0, got %v", unmarshaled["jsonrpc"])
}
if unmarshaled["method"] != "getTask" {
t.Errorf("expected method=getTask, got %v", unmarshaled["method"])
}
if unmarshaled["id"].(float64) != 1 {
t.Errorf("expected id=1, got %v", unmarshaled["id"])
}
}
func TestJSONRPCRequest_MarshalWithoutParams(t *testing.T) {
req := JSONRPCRequest{
JSONRPC: "2.0",
Method: "getAllProjects",
ID: 1,
}
data, err := json.Marshal(req)
if err != nil {
t.Fatalf("failed to marshal request: %v", err)
}
var unmarshaled map[string]interface{}
if err := json.Unmarshal(data, &unmarshaled); err != nil {
t.Fatalf("failed to unmarshal request: %v", err)
}
if _, exists := unmarshaled["params"]; exists {
t.Error("params should be omitted when nil")
}
}
func TestJSONRPCResponse_Unmarshal(t *testing.T) {
data := `{"jsonrpc":"2.0","id":1,"result":{"id":42,"title":"Test Task"}}`
var resp JSONRPCResponse
if err := json.Unmarshal([]byte(data), &resp); err != nil {
t.Fatalf("failed to unmarshal response: %v", err)
}
if resp.JSONRPC != "2.0" {
t.Errorf("expected jsonrpc=2.0, got %v", resp.JSONRPC)
}
if resp.ID != 1 {
t.Errorf("expected id=1, got %v", resp.ID)
}
if resp.Error != nil {
t.Error("expected no error")
}
if resp.Result == nil {
t.Error("expected result to be present")
}
}
func TestJSONRPCResponse_UnmarshalError(t *testing.T) {
data := `{"jsonrpc":"2.0","id":1,"error":{"code":-32600,"message":"Invalid Request"}}`
var resp JSONRPCResponse
if err := json.Unmarshal([]byte(data), &resp); err != nil {
t.Fatalf("failed to unmarshal response: %v", err)
}
if resp.Error == nil {
t.Fatal("expected error to be present")
}
if resp.Error.Code != -32600 {
t.Errorf("expected error code=-32600, got %v", resp.Error.Code)
}
if resp.Error.Message != "Invalid Request" {
t.Errorf("expected error message='Invalid Request', got %v", resp.Error.Message)
}
}
func TestJSONRPCError_Error(t *testing.T) {
err := &JSONRPCError{
Code: -32600,
Message: "Invalid Request",
}
expected := "JSON-RPC error (code -32600): Invalid Request"
if err.Error() != expected {
t.Errorf("expected %q, got %q", expected, err.Error())
}
}
func TestNextRequestID_Increments(t *testing.T) {
// Get the current counter value
initial := nextRequestID()
// Verify increments
for i := int64(1); i <= 5; i++ {
got := nextRequestID()
expected := initial + i
if got != expected {
t.Errorf("expected %d, got %d", expected, got)
}
}
}
func TestNextRequestID_ThreadSafe(t *testing.T) {
const goroutines = 100
const iterations = 100
var wg sync.WaitGroup
ids := make(chan int64, goroutines*iterations)
for i := 0; i < goroutines; i++ {
wg.Add(1)
go func() {
defer wg.Done()
for j := 0; j < iterations; j++ {
ids <- nextRequestID()
}
}()
}
wg.Wait()
close(ids)
// Collect all IDs and check for uniqueness
seen := make(map[int64]bool)
for id := range ids {
if seen[id] {
t.Errorf("duplicate request ID: %d", id)
}
seen[id] = true
}
if len(seen) != goroutines*iterations {
t.Errorf("expected %d unique IDs, got %d", goroutines*iterations, len(seen))
}
}
func TestClient_Call_Success(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
t.Errorf("expected POST, got %s", r.Method)
}
if r.URL.Path != "/jsonrpc.php" {
t.Errorf("expected /jsonrpc.php, got %s", r.URL.Path)
}
var req JSONRPCRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
t.Fatalf("failed to decode request: %v", err)
}
if req.JSONRPC != "2.0" {
t.Errorf("expected jsonrpc=2.0, got %s", req.JSONRPC)
}
if req.Method != "getTask" {
t.Errorf("expected method=getTask, got %s", req.Method)
}
resp := JSONRPCResponse{
JSONRPC: "2.0",
ID: req.ID,
Result: json.RawMessage(`{"id":42,"title":"Test Task"}`),
}
json.NewEncoder(w).Encode(resp)
}))
defer server.Close()
client := NewClient(server.URL).WithAPIToken("test-token")
var result struct {
ID int `json:"id"`
Title string `json:"title"`
}
err := client.call(context.Background(), "getTask", map[string]int{"task_id": 42}, &result)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if result.ID != 42 {
t.Errorf("expected id=42, got %d", result.ID)
}
if result.Title != "Test Task" {
t.Errorf("expected title='Test Task', got %s", result.Title)
}
}
func TestClient_Call_APIError(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
var req JSONRPCRequest
json.NewDecoder(r.Body).Decode(&req)
resp := JSONRPCResponse{
JSONRPC: "2.0",
ID: req.ID,
Error: &JSONRPCError{
Code: -32600,
Message: "Invalid Request",
},
}
json.NewEncoder(w).Encode(resp)
}))
defer server.Close()
client := NewClient(server.URL).WithAPIToken("test-token")
var result interface{}
err := client.call(context.Background(), "invalidMethod", nil, &result)
if err == nil {
t.Fatal("expected error")
}
apiErr, ok := err.(*APIError)
if !ok {
t.Fatalf("expected *APIError, got %T", err)
}
if apiErr.Code != -32600 {
t.Errorf("expected code=-32600, got %d", apiErr.Code)
}
}
func TestClient_Call_Unauthorized(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusUnauthorized)
}))
defer server.Close()
client := NewClient(server.URL).WithAPIToken("invalid-token")
var result interface{}
err := client.call(context.Background(), "getTask", nil, &result)
if err != ErrUnauthorized {
t.Errorf("expected ErrUnauthorized, got %v", err)
}
}
func TestClient_Call_Forbidden(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusForbidden)
}))
defer server.Close()
client := NewClient(server.URL).WithAPIToken("test-token")
var result interface{}
err := client.call(context.Background(), "getTask", nil, &result)
if err != ErrForbidden {
t.Errorf("expected ErrForbidden, got %v", err)
}
}
func TestClient_Call_ContextCanceled(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// Simulate slow response
select {}
}))
defer server.Close()
client := NewClient(server.URL).WithAPIToken("test-token")
ctx, cancel := context.WithCancel(context.Background())
cancel() // Cancel immediately
var result interface{}
err := client.call(ctx, "getTask", nil, &result)
if err == nil {
t.Fatal("expected error due to canceled context")
}
}
func TestClient_Call_SubdirectoryInstallation(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.URL.Path != "/kanboard/jsonrpc.php" {
t.Errorf("expected /kanboard/jsonrpc.php, got %s", r.URL.Path)
}
var req JSONRPCRequest
json.NewDecoder(r.Body).Decode(&req)
resp := JSONRPCResponse{
JSONRPC: "2.0",
ID: req.ID,
Result: json.RawMessage(`true`),
}
json.NewEncoder(w).Encode(resp)
}))
defer server.Close()
// Client with subdirectory path
client := NewClient(server.URL + "/kanboard").WithAPIToken("test-token")
var result bool
err := client.call(context.Background(), "getVersion", nil, &result)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
}
func TestClient_Call_TrailingSlashHandling(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.URL.Path != "/jsonrpc.php" {
t.Errorf("expected /jsonrpc.php, got %s", r.URL.Path)
}
var req JSONRPCRequest
json.NewDecoder(r.Body).Decode(&req)
resp := JSONRPCResponse{
JSONRPC: "2.0",
ID: req.ID,
Result: json.RawMessage(`true`),
}
json.NewEncoder(w).Encode(resp)
}))
defer server.Close()
// Client with trailing slash
client := NewClient(server.URL + "/").WithAPIToken("test-token")
var result bool
err := client.call(context.Background(), "getVersion", nil, &result)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
}
func TestClient_Call_AuthHeaderSent(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
username, password, ok := r.BasicAuth()
if !ok {
t.Error("expected Basic Auth header")
}
if username != "jsonrpc" {
t.Errorf("expected username=jsonrpc, got %s", username)
}
if password != "test-token" {
t.Errorf("expected password=test-token, got %s", password)
}
var req JSONRPCRequest
json.NewDecoder(r.Body).Decode(&req)
resp := JSONRPCResponse{
JSONRPC: "2.0",
ID: req.ID,
Result: json.RawMessage(`true`),
}
json.NewEncoder(w).Encode(resp)
}))
defer server.Close()
client := NewClient(server.URL).WithAPIToken("test-token")
var result bool
err := client.call(context.Background(), "getVersion", nil, &result)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
}