clickclack/apps/api/internal/httpapi/mutations_test.go
2026-05-08 05:36:16 +01:00

129 lines
5.0 KiB
Go

package httpapi
import (
"bytes"
"context"
"encoding/json"
"net/http"
"net/http/httptest"
"path/filepath"
"testing"
"github.com/openclaw/clickclack/apps/api/internal/realtime"
"github.com/openclaw/clickclack/apps/api/internal/store"
sqlitestore "github.com/openclaw/clickclack/apps/api/internal/store/sqlite"
)
func TestMutationAndEphemeralEndpoints(t *testing.T) {
t.Parallel()
ctx := context.Background()
dataDir := t.TempDir()
st, err := sqlitestore.Open("sqlite://" + filepath.Join(dataDir, "clickclack.db"))
if err != nil {
t.Fatal(err)
}
t.Cleanup(func() { _ = st.Close() })
if err := st.Migrate(ctx); err != nil {
t.Fatal(err)
}
owner, err := st.EnsureBootstrap(ctx, "Owner", "owner@example.com")
if err != nil {
t.Fatal(err)
}
workspaces, err := st.ListWorkspaces(ctx, owner.ID)
if err != nil {
t.Fatal(err)
}
channels, err := st.ListChannels(ctx, workspaces[0].ID, owner.ID)
if err != nil {
t.Fatal(err)
}
server := httptest.NewServer(New(st, realtime.NewHub(), Options{UploadDir: filepath.Join(dataDir, "uploads")}).Handler())
t.Cleanup(server.Close)
updatedChannel := patchJSON[struct {
Channel store.Channel `json:"channel"`
Event store.Event `json:"event"`
}](t, server.URL+"/api/channels/"+channels[0].ID, map[string]any{"name": "dock"})
if updatedChannel.Channel.Name != "dock" || updatedChannel.Event.Type != "channel.updated" {
t.Fatalf("unexpected channel update: %#v", updatedChannel)
}
message := postJSON[struct {
Message store.Message `json:"message"`
}](t, server.URL+"/api/channels/"+channels[0].ID+"/messages", map[string]string{"body": "original"}).Message
updatedMessage := patchJSON[struct {
Message store.Message `json:"message"`
Event store.Event `json:"event"`
}](t, server.URL+"/api/messages/"+message.ID, map[string]string{"body": "edited"})
if updatedMessage.Message.Body != "edited" || updatedMessage.Event.Type != "message.updated" {
t.Fatalf("unexpected message update: %#v", updatedMessage)
}
deletedMessage := deleteJSONBody[struct {
Message store.Message `json:"message"`
Event store.Event `json:"event"`
}](t, server.URL+"/api/messages/"+message.ID)
if deletedMessage.Message.DeletedAt == nil || deletedMessage.Event.Type != "message.deleted" {
t.Fatalf("unexpected message delete: %#v", deletedMessage)
}
ephemeral := postJSON[struct {
Event store.Event `json:"event"`
}](t, server.URL+"/api/realtime/ephemeral", map[string]any{"workspace_id": workspaces[0].ID, "channel_id": channels[0].ID, "type": "typing.started"})
if ephemeral.Event.Type != "typing.started" || ephemeral.Event.Cursor != "" {
t.Fatalf("unexpected ephemeral event: %#v", ephemeral.Event)
}
presence := postJSON[struct {
Event store.Event `json:"event"`
}](t, server.URL+"/api/realtime/ephemeral", map[string]any{"workspace_id": workspaces[0].ID, "type": "presence.changed", "payload": map[string]any{"status": "afk"}})
if presence.Event.Type != "presence.changed" {
t.Fatalf("unexpected presence event: %#v", presence.Event)
}
expectStatus(t, http.MethodPatch, server.URL+"/api/channels/"+channels[0].ID, bytes.NewReader([]byte(`{`)), http.StatusBadRequest)
expectStatus(t, http.MethodPatch, server.URL+"/api/channels/missing", bytes.NewReader([]byte(`{"name":"missing"}`)), http.StatusBadRequest)
expectStatus(t, http.MethodPatch, server.URL+"/api/messages/"+message.ID, bytes.NewReader([]byte(`{`)), http.StatusBadRequest)
expectStatus(t, http.MethodPatch, server.URL+"/api/messages/"+message.ID, bytes.NewReader([]byte(`{"body":" "}`)), http.StatusBadRequest)
expectStatus(t, http.MethodDelete, server.URL+"/api/messages/missing", nil, http.StatusBadRequest)
expectStatus(t, http.MethodPost, server.URL+"/api/realtime/ephemeral", bytes.NewReader([]byte(`{`)), http.StatusBadRequest)
expectStatus(t, http.MethodPost, server.URL+"/api/realtime/ephemeral", bytes.NewReader([]byte(`{"workspace_id":"`+workspaces[0].ID+`","type":"bad"}`)), http.StatusBadRequest)
expectStatus(t, http.MethodPost, server.URL+"/api/realtime/ephemeral", bytes.NewReader([]byte(`{"workspace_id":"missing","type":"typing.started"}`)), http.StatusForbidden)
}
func patchJSON[T any](t *testing.T, endpoint string, body any) T {
t.Helper()
payload, err := json.Marshal(body)
if err != nil {
t.Fatal(err)
}
req, err := http.NewRequest(http.MethodPatch, endpoint, bytes.NewReader(payload))
if err != nil {
t.Fatal(err)
}
req.Header.Set("Content-Type", "application/json")
return doJSON[T](t, req)
}
func deleteJSONBody[T any](t *testing.T, endpoint string) T {
t.Helper()
req, err := http.NewRequest(http.MethodDelete, endpoint, nil)
if err != nil {
t.Fatal(err)
}
return doJSON[T](t, req)
}
func doJSON[T any](t *testing.T, req *http.Request) T {
t.Helper()
resp, err := http.DefaultClient.Do(req)
if err != nil {
t.Fatal(err)
}
defer resp.Body.Close()
if resp.StatusCode >= 300 {
t.Fatalf("%s %s: %s", req.Method, req.URL, resp.Status)
}
var out T
if err := json.NewDecoder(resp.Body).Decode(&out); err != nil {
t.Fatal(err)
}
return out
}