pkg/client/client_test.go
491 linesgo
package client

import (
	"context"
	"encoding/json"
	"net/http"
	"net/http/httptest"
	"testing"
	"time"
)

func newTestServer(t *testing.T, mux *http.ServeMux) *httptest.Server {
	t.Helper()
	ts := httptest.NewServer(mux)
	t.Cleanup(ts.Close)
	return ts
}

func writeJSON(t *testing.T, w http.ResponseWriter, v any) {
	t.Helper()
	w.Header().Set("Content-Type", "application/json")
	if err := json.NewEncoder(w).Encode(v); err != nil {
		t.Fatalf("failed to write JSON response: %v", err)
	}
}

func setupOAuthMux(t *testing.T, mux *http.ServeMux) {
	t.Helper()
	mux.HandleFunc("/oauth/token", func(w http.ResponseWriter, r *http.Request) {
		writeJSON(t, w, TokenResponse{
			AccessToken: "test-token",
			ExpiresIn:   3600,
			TokenType:   "Bearer",
		})
	})
}

func newTestClient(t *testing.T, mux *http.ServeMux) *Client {
	t.Helper()
	setupOAuthMux(t, mux)
	ts := newTestServer(t, mux)
	c, err := New(context.Background(), "test-id", "test-secret", ts.URL)
	if err != nil {
		t.Fatalf("unexpected error creating client: %v", err)
	}
	return c
}

func TestGetUsers(t *testing.T) {
	mux := http.NewServeMux()

	now := time.Now().UTC().Truncate(time.Second)
	testUsers := []User{
		{ID: "u1", Email: "alice@test.com", FirstName: "Alice", LastName: "Smith", Username: "alice", Status: "active", CreatedAt: now},
		{ID: "u2", Email: "bob@test.com", FirstName: "Bob", LastName: "Jones", Username: "bob", Status: "disabled", CreatedAt: now},
	}

	mux.HandleFunc("/api/users", func(w http.ResponseWriter, r *http.Request) {
		cursor := r.URL.Query().Get("cursor")
		if cursor == "" {
			writeJSON(t, w, ListResponse[User]{Data: testUsers[:1], NextCursor: "page2"})
			return
		}
		writeJSON(t, w, ListResponse[User]{Data: testUsers[1:], NextCursor: ""})
	})

	c := newTestClient(t, mux)

	t.Run("first page", func(t *testing.T) {
		users, next, err := c.GetUsers(context.Background(), "")
		if err != nil {
			t.Fatalf("unexpected error: %v", err)
		}
		if len(users) != 1 {
			t.Fatalf("expected 1 user, got %d", len(users))
		}
		if users[0].ID != "u1" {
			t.Errorf("expected user ID u1, got %s", users[0].ID)
		}
		if next != "page2" {
			t.Errorf("expected next cursor page2, got %s", next)
		}
	})

	t.Run("second page", func(t *testing.T) {
		users, next, err := c.GetUsers(context.Background(), "page2")
		if err != nil {
			t.Fatalf("unexpected error: %v", err)
		}
		if len(users) != 1 {
			t.Fatalf("expected 1 user, got %d", len(users))
		}
		if users[0].ID != "u2" {
			t.Errorf("expected user ID u2, got %s", users[0].ID)
		}
		if next != "" {
			t.Errorf("expected empty next cursor, got %s", next)
		}
	})
}

func TestGetRoles(t *testing.T) {
	mux := http.NewServeMux()
	mux.HandleFunc("/api/roles", func(w http.ResponseWriter, r *http.Request) {
		writeJSON(t, w, ListResponse[Role]{
			Data: []Role{
				{ID: "r1", Name: "Single Ride", Description: "One-time journey on any standard route"},
				{ID: "r2", Name: "Standard Day", Description: "Unlimited standard travel for the day"},
			},
		})
	})

	c := newTestClient(t, mux)
	roles, next, err := c.GetRoles(context.Background(), "")
	if err != nil {
		t.Fatalf("unexpected error: %v", err)
	}
	if len(roles) != 2 {
		t.Fatalf("expected 2 roles, got %d", len(roles))
	}
	if roles[0].Name != "Single Ride" {
		t.Errorf("expected Single Ride, got %s", roles[0].Name)
	}
	if next != "" {
		t.Errorf("expected empty next cursor, got %s", next)
	}
}

func TestGetGroups(t *testing.T) {
	mux := http.NewServeMux()
	mux.HandleFunc("/api/groups", func(w http.ResponseWriter, r *http.Request) {
		writeJSON(t, w, ListResponse[Group]{
			Data: []Group{{ID: "g1", Name: "Regional Pass", Description: "Monthly pass for standard daily travel"}},
		})
	})

	c := newTestClient(t, mux)
	groups, next, err := c.GetGroups(context.Background(), "")
	if err != nil {
		t.Fatalf("unexpected error: %v", err)
	}
	if len(groups) != 1 {
		t.Fatalf("expected 1 group, got %d", len(groups))
	}
	if groups[0].Name != "Regional Pass" {
		t.Errorf("expected Regional Pass, got %s", groups[0].Name)
	}
	if next != "" {
		t.Errorf("expected empty next cursor, got %s", next)
	}
}

func TestGetRoleMembers(t *testing.T) {
	mux := http.NewServeMux()
	mux.HandleFunc("/api/roles/r1/members", func(w http.ResponseWriter, r *http.Request) {
		writeJSON(t, w, ListResponse[Member]{Data: []Member{{UserID: "u1"}, {UserID: "u2"}}})
	})

	c := newTestClient(t, mux)
	members, next, err := c.GetRoleMembers(context.Background(), "r1", "")
	if err != nil {
		t.Fatalf("unexpected error: %v", err)
	}
	if len(members) != 2 {
		t.Fatalf("expected 2 members, got %d", len(members))
	}
	if next != "" {
		t.Errorf("expected empty next cursor, got %s", next)
	}
}

func TestGetGroupMembers(t *testing.T) {
	mux := http.NewServeMux()
	mux.HandleFunc("/api/groups/g1/members", func(w http.ResponseWriter, r *http.Request) {
		writeJSON(t, w, ListResponse[Member]{Data: []Member{{UserID: "u1"}}})
	})

	c := newTestClient(t, mux)
	members, next, err := c.GetGroupMembers(context.Background(), "g1", "")
	if err != nil {
		t.Fatalf("unexpected error: %v", err)
	}
	if len(members) != 1 {
		t.Fatalf("expected 1 member, got %d", len(members))
	}
	if members[0].UserID != "u1" {
		t.Errorf("expected user ID u1, got %s", members[0].UserID)
	}
	if next != "" {
		t.Errorf("expected empty next cursor, got %s", next)
	}
}

func TestCreateUser(t *testing.T) {
	mux := http.NewServeMux()
	mux.HandleFunc("/api/users", func(w http.ResponseWriter, r *http.Request) {
		if r.Method != http.MethodPost {
			http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
			return
		}

		var req CreateUserRequest
		if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
			http.Error(w, "bad request", http.StatusBadRequest)
			return
		}

		w.Header().Set("Content-Type", "application/json")
		w.WriteHeader(http.StatusCreated)
		if err := json.NewEncoder(w).Encode(SingleResponse[User]{
			Data: User{
				ID: "u-new", Email: req.Email,
				FirstName: req.FirstName, LastName: req.LastName,
				Username: req.Username, Status: "active",
				CreatedAt: time.Now().UTC(),
			},
		}); err != nil {
			t.Errorf("failed to encode response: %v", err)
		}
	})

	c := newTestClient(t, mux)
	user, err := c.CreateUser(context.Background(), &CreateUserRequest{
		Email: "new@test.com", FirstName: "New", LastName: "User", Username: "newuser",
	})
	if err != nil {
		t.Fatalf("unexpected error: %v", err)
	}
	if user.ID != "u-new" {
		t.Errorf("expected user ID u-new, got %s", user.ID)
	}
	if user.Email != "new@test.com" {
		t.Errorf("expected email new@test.com, got %s", user.Email)
	}
}

func TestAddRoleMember(t *testing.T) {
	mux := http.NewServeMux()

	var calledPath string
	var calledMethod string
	mux.HandleFunc("/api/roles/r1/members/u1", func(w http.ResponseWriter, r *http.Request) {
		calledPath = r.URL.Path
		calledMethod = r.Method
		w.WriteHeader(http.StatusNoContent)
	})

	c := newTestClient(t, mux)
	if err := c.AddRoleMember(context.Background(), "r1", "u1"); err != nil {
		t.Fatalf("unexpected error: %v", err)
	}
	if calledMethod != http.MethodPut {
		t.Errorf("expected PUT, got %s", calledMethod)
	}
	if calledPath != "/api/roles/r1/members/u1" {
		t.Errorf("unexpected path: %s", calledPath)
	}
}

func TestRemoveRoleMember(t *testing.T) {
	mux := http.NewServeMux()
	mux.HandleFunc("/api/roles/r1/members/u1", func(w http.ResponseWriter, r *http.Request) {
		if r.Method != http.MethodDelete {
			http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
			return
		}
		w.WriteHeader(http.StatusNoContent)
	})

	c := newTestClient(t, mux)
	if err := c.RemoveRoleMember(context.Background(), "r1", "u1"); err != nil {
		t.Fatalf("unexpected error: %v", err)
	}
}

func TestAddGroupMember(t *testing.T) {
	mux := http.NewServeMux()
	mux.HandleFunc("/api/groups/g1/members/u1", func(w http.ResponseWriter, r *http.Request) {
		w.WriteHeader(http.StatusNoContent)
	})

	c := newTestClient(t, mux)
	if err := c.AddGroupMember(context.Background(), "g1", "u1"); err != nil {
		t.Fatalf("unexpected error: %v", err)
	}
}

func TestRemoveGroupMember(t *testing.T) {
	mux := http.NewServeMux()
	mux.HandleFunc("/api/groups/g1/members/u1", func(w http.ResponseWriter, r *http.Request) {
		if r.Method != http.MethodDelete {
			http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
			return
		}
		w.WriteHeader(http.StatusNoContent)
	})

	c := newTestClient(t, mux)
	if err := c.RemoveGroupMember(context.Background(), "g1", "u1"); err != nil {
		t.Fatalf("unexpected error: %v", err)
	}
}

func TestGetCurrentUser(t *testing.T) {
	mux := http.NewServeMux()
	mux.HandleFunc("/api/users/me", func(w http.ResponseWriter, r *http.Request) {
		writeJSON(t, w, SingleResponse[User]{
			Data: User{
				ID: "u-me", Email: "me@test.com",
				FirstName: "Test", LastName: "User",
				Username: "testuser", Status: "active",
				CreatedAt: time.Now().UTC(),
			},
		})
	})

	c := newTestClient(t, mux)
	user, err := c.GetCurrentUser(context.Background())
	if err != nil {
		t.Fatalf("unexpected error: %v", err)
	}
	if user.ID != "u-me" {
		t.Errorf("expected user ID u-me, got %s", user.ID)
	}
}

func TestUpdateUser(t *testing.T) {
	mux := http.NewServeMux()
	mux.HandleFunc("/api/users/", func(w http.ResponseWriter, r *http.Request) {
		if r.Method != http.MethodPatch {
			http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
			return
		}

		var attrs map[string]string
		if err := json.NewDecoder(r.Body).Decode(&attrs); err != nil {
			http.Error(w, "bad request", http.StatusBadRequest)
			return
		}

		status := "active"
		if v, ok := attrs["status"]; ok {
			status = v
		}
		dept := ""
		if v, ok := attrs["department"]; ok {
			dept = v
		}

		writeJSON(t, w, SingleResponse[User]{
			Data: User{
				ID: "u1", Email: "alice@test.com",
				FirstName: "Alice", LastName: "Smith",
				Username: "alice", Status: status,
				Department: dept,
			},
		})
	})

	c := newTestClient(t, mux)

	t.Run("update status", func(t *testing.T) {
		user, err := c.UpdateUser(context.Background(), "u1", map[string]string{"status": "inactive"})
		if err != nil {
			t.Fatalf("unexpected error: %v", err)
		}
		if user.Status != "inactive" {
			t.Errorf("expected status inactive, got %s", user.Status)
		}
	})

	t.Run("update department", func(t *testing.T) {
		user, err := c.UpdateUser(context.Background(), "u1", map[string]string{"department": "Engineering"})
		if err != nil {
			t.Fatalf("unexpected error: %v", err)
		}
		if user.Department != "Engineering" {
			t.Errorf("expected department Engineering, got %s", user.Department)
		}
	})
}

func TestGetUser(t *testing.T) {
	mux := http.NewServeMux()
	mux.HandleFunc("/api/users/", func(w http.ResponseWriter, r *http.Request) {
		writeJSON(t, w, SingleResponse[User]{
			Data: User{
				ID: "u1", Email: "alice@test.com",
				FirstName: "Alice", LastName: "Smith",
				Username: "alice", Status: "active",
			},
		})
	})

	c := newTestClient(t, mux)
	user, err := c.GetUser(context.Background(), "u1")
	if err != nil {
		t.Fatalf("unexpected error: %v", err)
	}
	if user.ID != "u1" {
		t.Errorf("expected user ID u1, got %s", user.ID)
	}
	if user.Email != "alice@test.com" {
		t.Errorf("expected email alice@test.com, got %s", user.Email)
	}
}

func TestGetRole(t *testing.T) {
	mux := http.NewServeMux()
	mux.HandleFunc("/api/roles/", func(w http.ResponseWriter, r *http.Request) {
		writeJSON(t, w, SingleResponse[Role]{
			Data: Role{ID: "r1", Name: "Single Ride", Description: "One-time journey on any standard route"},
		})
	})

	c := newTestClient(t, mux)
	role, err := c.GetRole(context.Background(), "r1")
	if err != nil {
		t.Fatalf("unexpected error: %v", err)
	}
	if role.ID != "r1" {
		t.Errorf("expected role ID r1, got %s", role.ID)
	}
	if role.Name != "Single Ride" {
		t.Errorf("expected role name Single Ride, got %s", role.Name)
	}
}

func TestGetGroup(t *testing.T) {
	mux := http.NewServeMux()
	mux.HandleFunc("/api/groups/", func(w http.ResponseWriter, r *http.Request) {
		writeJSON(t, w, SingleResponse[Group]{
			Data: Group{ID: "g1", Name: "Regional Pass", Description: "Monthly pass for standard daily travel"},
		})
	})

	c := newTestClient(t, mux)
	group, err := c.GetGroup(context.Background(), "g1")
	if err != nil {
		t.Fatalf("unexpected error: %v", err)
	}
	if group.ID != "g1" {
		t.Errorf("expected group ID g1, got %s", group.ID)
	}
	if group.Name != "Regional Pass" {
		t.Errorf("expected group name Regional Pass, got %s", group.Name)
	}
}

func TestGetRoleGroups(t *testing.T) {
	mux := http.NewServeMux()
	mux.HandleFunc("/api/roles/r2/groups", func(w http.ResponseWriter, r *http.Request) {
		writeJSON(t, w, ListResponse[GroupAssignment]{
			Data: []GroupAssignment{{GroupID: "g1"}},
		})
	})

	c := newTestClient(t, mux)
	groups, err := c.GetRoleGroups(context.Background(), "r2")
	if err != nil {
		t.Fatalf("unexpected error: %v", err)
	}
	if len(groups) != 1 {
		t.Fatalf("expected 1 group assignment, got %d", len(groups))
	}
	if groups[0].GroupID != "g1" {
		t.Errorf("expected group ID g1, got %s", groups[0].GroupID)
	}
}

func TestGetRoleRoles(t *testing.T) {
	mux := http.NewServeMux()
	mux.HandleFunc("/api/roles/r3/roles", func(w http.ResponseWriter, r *http.Request) {
		writeJSON(t, w, ListResponse[RoleAssignment]{
			Data: []RoleAssignment{{RoleID: "r4"}},
		})
	})

	c := newTestClient(t, mux)
	roles, err := c.GetRoleRoles(context.Background(), "r3")
	if err != nil {
		t.Fatalf("unexpected error: %v", err)
	}
	if len(roles) != 1 {
		t.Fatalf("expected 1 role assignment, got %d", len(roles))
	}
	if roles[0].RoleID != "r4" {
		t.Errorf("expected role ID r4, got %s", roles[0].RoleID)
	}
}