mirror of
https://github.com/coder/coder.git
synced 2026-09-24 15:04:27 +08:00
feat: add chat sharing foundation (#25041)
This commit is contained in:
@@ -14291,6 +14291,233 @@ func requireSDKError(t *testing.T, err error, expectedStatus int) *codersdk.Erro
|
||||
return sdkErr
|
||||
}
|
||||
|
||||
func TestChatReadOnlySharedWriteHandlers(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
const sharedChatText = "read only shared chat"
|
||||
|
||||
setup := func(t *testing.T) (
|
||||
ctx context.Context,
|
||||
ownerClient *codersdk.ExperimentalClient,
|
||||
sharedClient *codersdk.ExperimentalClient,
|
||||
chat codersdk.Chat,
|
||||
db database.Store,
|
||||
) {
|
||||
t.Helper()
|
||||
|
||||
ctx = testutil.Context(t, testutil.WaitLong)
|
||||
ownerClient, db = newChatClientWithDatabase(t)
|
||||
owner := coderdtest.CreateFirstUser(t, ownerClient.Client)
|
||||
_ = createChatModelConfig(t, ownerClient)
|
||||
sharedRaw, sharedUser := coderdtest.CreateAnotherUser(
|
||||
t,
|
||||
ownerClient.Client,
|
||||
owner.OrganizationID,
|
||||
rbac.ScopedRoleAgentsAccess(owner.OrganizationID),
|
||||
)
|
||||
sharedClient = codersdk.NewExperimentalClient(sharedRaw)
|
||||
|
||||
var err error
|
||||
chat, err = ownerClient.CreateChat(ctx, codersdk.CreateChatRequest{
|
||||
OrganizationID: owner.OrganizationID,
|
||||
Content: []codersdk.ChatInputPart{{
|
||||
Type: codersdk.ChatInputPartTypeText,
|
||||
Text: sharedChatText,
|
||||
}},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
err = db.UpdateChatACLByID(dbauthz.As(ctx, rbac.Subject{
|
||||
ID: owner.UserID.String(),
|
||||
Roles: rbac.RoleIdentifiers{rbac.RoleOwner()},
|
||||
Scope: rbac.ScopeAll,
|
||||
}), database.UpdateChatACLByIDParams{
|
||||
ID: chat.ID,
|
||||
UserACL: database.ChatACL{
|
||||
sharedUser.ID.String(): database.ChatACLEntry{Permissions: []policy.Action{policy.ActionRead}},
|
||||
},
|
||||
GroupACL: database.ChatACL{},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
return ctx, ownerClient, sharedClient, chat, db
|
||||
}
|
||||
|
||||
t.Run("GetChatAndMessages", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
ctx, _, sharedClient, chat, _ := setup(t)
|
||||
|
||||
gotChat, err := sharedClient.GetChat(ctx, chat.ID)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, chat.ID, gotChat.ID)
|
||||
|
||||
messagesResult, err := sharedClient.GetChatMessages(ctx, chat.ID, nil)
|
||||
require.NoError(t, err)
|
||||
require.NotEmpty(t, messagesResult.Messages)
|
||||
|
||||
foundUserMessage := false
|
||||
for _, message := range messagesResult.Messages {
|
||||
if message.Role != codersdk.ChatMessageRoleUser {
|
||||
continue
|
||||
}
|
||||
for _, part := range message.Content {
|
||||
if part.Type == codersdk.ChatMessagePartTypeText && part.Text == sharedChatText {
|
||||
foundUserMessage = true
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
require.True(t, foundUserMessage)
|
||||
})
|
||||
|
||||
t.Run("PatchChat", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
ctx, _, sharedClient, chat, _ := setup(t)
|
||||
err := sharedClient.UpdateChat(ctx, chat.ID, codersdk.UpdateChatRequest{
|
||||
Archived: ptr.Ref(true),
|
||||
})
|
||||
|
||||
requireSDKError(t, err, http.StatusNotFound)
|
||||
})
|
||||
|
||||
t.Run("PatchChatMessage", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
ctx, ownerClient, sharedClient, chat, _ := setup(t)
|
||||
messagesResult, err := ownerClient.GetChatMessages(ctx, chat.ID, nil)
|
||||
require.NoError(t, err)
|
||||
var userMessageID int64
|
||||
for _, msg := range messagesResult.Messages {
|
||||
if msg.Role == codersdk.ChatMessageRoleUser {
|
||||
userMessageID = msg.ID
|
||||
break
|
||||
}
|
||||
}
|
||||
require.NotZero(t, userMessageID)
|
||||
|
||||
_, err = sharedClient.EditChatMessage(ctx, chat.ID, userMessageID, codersdk.EditChatMessageRequest{
|
||||
Content: []codersdk.ChatInputPart{{
|
||||
Type: codersdk.ChatInputPartTypeText,
|
||||
Text: "read only user cannot edit",
|
||||
}},
|
||||
})
|
||||
|
||||
requireSDKError(t, err, http.StatusNotFound)
|
||||
})
|
||||
|
||||
t.Run("PostChatMessages", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
ctx, _, sharedClient, chat, _ := setup(t)
|
||||
_, err := sharedClient.CreateChatMessage(ctx, chat.ID, codersdk.CreateChatMessageRequest{
|
||||
Content: []codersdk.ChatInputPart{{
|
||||
Type: codersdk.ChatInputPartTypeText,
|
||||
Text: "read only user cannot send messages",
|
||||
}},
|
||||
})
|
||||
|
||||
requireSDKError(t, err, http.StatusNotFound)
|
||||
})
|
||||
|
||||
t.Run("PromoteChatQueuedMessage", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
ctx, _, sharedClient, chat, db := setup(t)
|
||||
queuedContent, err := json.Marshal([]codersdk.ChatMessagePart{
|
||||
codersdk.ChatMessageText("queued"),
|
||||
})
|
||||
require.NoError(t, err)
|
||||
queuedMessage, err := db.InsertChatQueuedMessage(
|
||||
dbauthz.AsSystemRestricted(ctx),
|
||||
database.InsertChatQueuedMessageParams{
|
||||
ChatID: chat.ID,
|
||||
Content: queuedContent,
|
||||
},
|
||||
)
|
||||
require.NoError(t, err)
|
||||
|
||||
res, err := sharedClient.Request(
|
||||
ctx,
|
||||
http.MethodPost,
|
||||
fmt.Sprintf("/api/experimental/chats/%s/queue/%d/promote", chat.ID, queuedMessage.ID),
|
||||
nil,
|
||||
)
|
||||
require.NoError(t, err)
|
||||
defer res.Body.Close()
|
||||
require.Equal(t, http.StatusNotFound, res.StatusCode)
|
||||
})
|
||||
|
||||
t.Run("PostChatToolResults", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
ctx, _, sharedClient, chat, _ := setup(t)
|
||||
err := sharedClient.SubmitToolResults(ctx, chat.ID, codersdk.SubmitToolResultsRequest{
|
||||
Results: []codersdk.ToolResult{{
|
||||
ToolCallID: "call_read_only",
|
||||
Output: json.RawMessage(`"forbidden"`),
|
||||
}},
|
||||
})
|
||||
|
||||
requireSDKError(t, err, http.StatusNotFound)
|
||||
})
|
||||
|
||||
t.Run("DeleteChatQueuedMessage", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
ctx, _, sharedClient, chat, db := setup(t)
|
||||
queuedContent, err := json.Marshal([]codersdk.ChatMessagePart{
|
||||
codersdk.ChatMessageText("queued"),
|
||||
})
|
||||
require.NoError(t, err)
|
||||
queuedMessage, err := db.InsertChatQueuedMessage(
|
||||
dbauthz.AsSystemRestricted(ctx),
|
||||
database.InsertChatQueuedMessageParams{
|
||||
ChatID: chat.ID,
|
||||
Content: queuedContent,
|
||||
},
|
||||
)
|
||||
require.NoError(t, err)
|
||||
|
||||
res, err := sharedClient.Request(
|
||||
ctx,
|
||||
http.MethodDelete,
|
||||
fmt.Sprintf("/api/experimental/chats/%s/queue/%d", chat.ID, queuedMessage.ID),
|
||||
nil,
|
||||
)
|
||||
require.NoError(t, err)
|
||||
defer res.Body.Close()
|
||||
require.Equal(t, http.StatusNotFound, res.StatusCode)
|
||||
})
|
||||
|
||||
t.Run("InterruptChat", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
ctx, _, sharedClient, chat, _ := setup(t)
|
||||
_, err := sharedClient.InterruptChat(ctx, chat.ID)
|
||||
|
||||
requireSDKError(t, err, http.StatusNotFound)
|
||||
})
|
||||
|
||||
t.Run("RegenerateChatTitle", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
ctx, _, sharedClient, chat, _ := setup(t)
|
||||
_, err := sharedClient.RegenerateChatTitle(ctx, chat.ID)
|
||||
|
||||
requireSDKError(t, err, http.StatusNotFound)
|
||||
})
|
||||
|
||||
t.Run("ProposeChatTitle", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
ctx, _, sharedClient, chat, _ := setup(t)
|
||||
_, err := sharedClient.ProposeChatTitle(ctx, chat.ID)
|
||||
|
||||
requireSDKError(t, err, http.StatusNotFound)
|
||||
})
|
||||
}
|
||||
|
||||
// TestChatOwnerOnlyWriteHandlers verifies that only the chat owner can
|
||||
// call handlers that trigger chat processing. Org admins pass the RBAC
|
||||
// ActionUpdate check (org-level permission) but must still be blocked
|
||||
|
||||
Reference in New Issue
Block a user