mirror of
https://github.com/coder/coder.git
synced 2026-06-02 20:48:20 +00:00
c650aabbef
My agent added `//nolint:testpackage` to a test file on one of my PRs. Again. This PR cleans it up across the entire repo and updates the in-repo conventions so future agents stop doing it. The repo already has a precedent for white-box tests that need to touch unexported symbols: `*_internal_test.go` (145+ existing files). The `testpackage` linter's default `skip-regexp` exempts that filename suffix, so the `//nolint:testpackage` directive is unnecessary in every case where someone reached for it. This PR renames 51 such files to `*_internal_test.go` via `git mv` so blame and history follow, and strips the dead directive from 2 files that were already correctly named (`coderd/oauth2provider/authorize_internal_test.go`, `coderd/x/chatd/advisor_internal_test.go`). `.claude/docs/TESTING.md` now documents the rule explicitly under *Test Package Naming*, which is imported into the root `AGENTS.md` via `@.claude/docs/TESTING.md`. The rule: prefer `package foo_test`; if you need internal access, rename the file to `*_internal_test.go` rather than adding a nolint directive.
269 lines
9.2 KiB
Go
269 lines
9.2 KiB
Go
package chatd
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"testing"
|
|
|
|
"github.com/google/uuid"
|
|
"github.com/stretchr/testify/require"
|
|
"go.uber.org/mock/gomock"
|
|
|
|
"github.com/coder/coder/v2/coderd/database"
|
|
"github.com/coder/coder/v2/coderd/database/dbmock"
|
|
"github.com/coder/coder/v2/coderd/x/chatd/chattool"
|
|
"github.com/coder/coder/v2/coderd/x/chatfiles"
|
|
"github.com/coder/coder/v2/codersdk"
|
|
)
|
|
|
|
func TestStoreChatAttachment_Success(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
ctrl := gomock.NewController(t)
|
|
db := dbmock.NewMockStore(ctrl)
|
|
tx := dbmock.NewMockStore(ctrl)
|
|
server := &Server{db: db}
|
|
|
|
chatID := uuid.New()
|
|
ownerID := uuid.New()
|
|
workspaceID := uuid.New()
|
|
orgID := uuid.New()
|
|
fileID := uuid.New()
|
|
chatSnapshot := database.Chat{
|
|
ID: chatID,
|
|
OwnerID: ownerID,
|
|
WorkspaceID: uuid.NullUUID{UUID: workspaceID, Valid: true},
|
|
}
|
|
|
|
expectStoreChatAttachmentTx(t, db, tx)
|
|
tx.EXPECT().GetWorkspaceByID(gomock.Any(), workspaceID).Return(database.Workspace{ID: workspaceID, OrganizationID: orgID}, nil)
|
|
tx.EXPECT().InsertChatFile(gomock.Any(), gomock.AssignableToTypeOf(database.InsertChatFileParams{})).DoAndReturn(
|
|
func(_ context.Context, arg database.InsertChatFileParams) (database.InsertChatFileRow, error) {
|
|
require.Equal(t, ownerID, arg.OwnerID)
|
|
require.Equal(t, orgID, arg.OrganizationID)
|
|
require.Equal(t, "build.log", arg.Name)
|
|
require.Equal(t, "text/plain", arg.Mimetype)
|
|
require.Equal(t, []byte("build output"), arg.Data)
|
|
return database.InsertChatFileRow{ID: fileID}, nil
|
|
},
|
|
)
|
|
tx.EXPECT().LinkChatFiles(gomock.Any(), database.LinkChatFilesParams{
|
|
ChatID: chatID,
|
|
MaxFileLinks: int32(codersdk.MaxChatFileIDs),
|
|
FileIds: []uuid.UUID{fileID},
|
|
}).Return(int32(0), nil)
|
|
|
|
attachment, err := server.storeChatAttachment(context.Background(), chatSnapshot, "build.log", "build.log", []byte("build output"))
|
|
require.NoError(t, err)
|
|
require.Equal(t, chattool.AttachmentMetadata{
|
|
FileID: fileID,
|
|
MediaType: "text/plain",
|
|
Name: "build.log",
|
|
}, attachment)
|
|
}
|
|
|
|
func TestStoreChatAttachment_UsesDetectNameForClassification(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
ctrl := gomock.NewController(t)
|
|
db := dbmock.NewMockStore(ctrl)
|
|
tx := dbmock.NewMockStore(ctrl)
|
|
server := &Server{db: db}
|
|
|
|
chatID := uuid.New()
|
|
ownerID := uuid.New()
|
|
workspaceID := uuid.New()
|
|
orgID := uuid.New()
|
|
fileID := uuid.New()
|
|
chatSnapshot := database.Chat{
|
|
ID: chatID,
|
|
OwnerID: ownerID,
|
|
WorkspaceID: uuid.NullUUID{UUID: workspaceID, Valid: true},
|
|
}
|
|
|
|
expectStoreChatAttachmentTx(t, db, tx)
|
|
tx.EXPECT().GetWorkspaceByID(gomock.Any(), workspaceID).Return(database.Workspace{ID: workspaceID, OrganizationID: orgID}, nil)
|
|
tx.EXPECT().InsertChatFile(gomock.Any(), gomock.AssignableToTypeOf(database.InsertChatFileParams{})).DoAndReturn(
|
|
func(_ context.Context, arg database.InsertChatFileParams) (database.InsertChatFileRow, error) {
|
|
require.Equal(t, "payload.txt", arg.Name)
|
|
require.Equal(t, "application/json", arg.Mimetype)
|
|
return database.InsertChatFileRow{ID: fileID}, nil
|
|
},
|
|
)
|
|
tx.EXPECT().LinkChatFiles(gomock.Any(), database.LinkChatFilesParams{
|
|
ChatID: chatID,
|
|
MaxFileLinks: int32(codersdk.MaxChatFileIDs),
|
|
FileIds: []uuid.UUID{fileID},
|
|
}).Return(int32(0), nil)
|
|
|
|
attachment, err := server.storeChatAttachment(context.Background(), chatSnapshot, "payload.txt", "report.json", []byte(`{"ok":true}`))
|
|
require.NoError(t, err)
|
|
require.Equal(t, "payload.txt", attachment.Name)
|
|
require.Equal(t, "application/json", attachment.MediaType)
|
|
}
|
|
|
|
func TestStoreChatAttachment_RejectsUnsupportedStoredFileTypeBeforeDBWork(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
ctrl := gomock.NewController(t)
|
|
db := dbmock.NewMockStore(ctrl)
|
|
server := &Server{db: db}
|
|
|
|
chatSnapshot := database.Chat{
|
|
ID: uuid.New(),
|
|
OwnerID: uuid.New(),
|
|
WorkspaceID: uuid.NullUUID{UUID: uuid.New(), Valid: true},
|
|
}
|
|
|
|
attachment, err := server.storeChatAttachment(
|
|
context.Background(),
|
|
chatSnapshot,
|
|
"evil.svg",
|
|
"evil.svg",
|
|
[]byte(`<svg xmlns="http://www.w3.org/2000/svg"><rect/></svg>`),
|
|
)
|
|
require.ErrorIs(t, err, chatfiles.ErrUnsupportedStoredFileType)
|
|
require.ErrorContains(t, err, "image/svg+xml")
|
|
require.Equal(t, chattool.AttachmentMetadata{}, attachment)
|
|
}
|
|
|
|
func TestStoreChatAttachment_NoWorkspace(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
ctrl := gomock.NewController(t)
|
|
db := dbmock.NewMockStore(ctrl)
|
|
server := &Server{db: db}
|
|
|
|
attachment, err := server.storeChatAttachment(context.Background(), database.Chat{}, "build.log", "build.log", []byte("build output"))
|
|
require.ErrorContains(t, err, "no workspace is associated")
|
|
require.Equal(t, chattool.AttachmentMetadata{}, attachment)
|
|
}
|
|
|
|
func TestStoreChatAttachment_WorkspaceLookupError(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
ctrl := gomock.NewController(t)
|
|
db := dbmock.NewMockStore(ctrl)
|
|
tx := dbmock.NewMockStore(ctrl)
|
|
server := &Server{db: db}
|
|
|
|
workspaceID := uuid.New()
|
|
chatSnapshot := database.Chat{
|
|
ID: uuid.New(),
|
|
OwnerID: uuid.New(),
|
|
WorkspaceID: uuid.NullUUID{UUID: workspaceID, Valid: true},
|
|
}
|
|
|
|
expectStoreChatAttachmentTx(t, db, tx)
|
|
tx.EXPECT().GetWorkspaceByID(gomock.Any(), workspaceID).Return(database.Workspace{}, context.DeadlineExceeded)
|
|
|
|
attachment, err := server.storeChatAttachment(context.Background(), chatSnapshot, "build.log", "build.log", []byte("build output"))
|
|
require.ErrorContains(t, err, "resolve workspace")
|
|
require.ErrorIs(t, err, context.DeadlineExceeded)
|
|
require.Equal(t, chattool.AttachmentMetadata{}, attachment)
|
|
}
|
|
|
|
func TestStoreChatAttachment_InsertError(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
ctrl := gomock.NewController(t)
|
|
db := dbmock.NewMockStore(ctrl)
|
|
tx := dbmock.NewMockStore(ctrl)
|
|
server := &Server{db: db}
|
|
|
|
workspaceID := uuid.New()
|
|
chatSnapshot := database.Chat{
|
|
ID: uuid.New(),
|
|
OwnerID: uuid.New(),
|
|
WorkspaceID: uuid.NullUUID{UUID: workspaceID, Valid: true},
|
|
}
|
|
|
|
expectStoreChatAttachmentTx(t, db, tx)
|
|
tx.EXPECT().GetWorkspaceByID(gomock.Any(), workspaceID).Return(database.Workspace{ID: workspaceID, OrganizationID: uuid.New()}, nil)
|
|
tx.EXPECT().InsertChatFile(gomock.Any(), gomock.Any()).Return(database.InsertChatFileRow{}, context.DeadlineExceeded)
|
|
|
|
attachment, err := server.storeChatAttachment(context.Background(), chatSnapshot, "build.log", "build.log", []byte("build output"))
|
|
require.ErrorContains(t, err, "insert chat file")
|
|
require.ErrorIs(t, err, context.DeadlineExceeded)
|
|
require.Equal(t, chattool.AttachmentMetadata{}, attachment)
|
|
}
|
|
|
|
func TestStoreChatAttachment_StrictCapError(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
ctrl := gomock.NewController(t)
|
|
db := dbmock.NewMockStore(ctrl)
|
|
tx := dbmock.NewMockStore(ctrl)
|
|
server := &Server{db: db}
|
|
|
|
chatID := uuid.New()
|
|
ownerID := uuid.New()
|
|
workspaceID := uuid.New()
|
|
orgID := uuid.New()
|
|
fileID := uuid.New()
|
|
chatSnapshot := database.Chat{
|
|
ID: chatID,
|
|
OwnerID: ownerID,
|
|
WorkspaceID: uuid.NullUUID{UUID: workspaceID, Valid: true},
|
|
}
|
|
|
|
expectStoreChatAttachmentTx(t, db, tx)
|
|
tx.EXPECT().GetWorkspaceByID(gomock.Any(), workspaceID).Return(database.Workspace{ID: workspaceID, OrganizationID: orgID}, nil)
|
|
tx.EXPECT().InsertChatFile(gomock.Any(), gomock.AssignableToTypeOf(database.InsertChatFileParams{})).Return(database.InsertChatFileRow{ID: fileID}, nil)
|
|
tx.EXPECT().LinkChatFiles(gomock.Any(), database.LinkChatFilesParams{
|
|
ChatID: chatID,
|
|
MaxFileLinks: int32(codersdk.MaxChatFileIDs),
|
|
FileIds: []uuid.UUID{fileID},
|
|
}).Return(int32(1), nil)
|
|
|
|
attachment, err := server.storeChatAttachment(context.Background(), chatSnapshot, "build.log", "build.log", []byte("build output"))
|
|
require.ErrorContains(t, err, fmt.Sprintf("chat already has the maximum of %d linked files", codersdk.MaxChatFileIDs))
|
|
require.Equal(t, chattool.AttachmentMetadata{}, attachment)
|
|
}
|
|
|
|
func TestStoreChatAttachment_LinkError(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
ctrl := gomock.NewController(t)
|
|
db := dbmock.NewMockStore(ctrl)
|
|
tx := dbmock.NewMockStore(ctrl)
|
|
server := &Server{db: db}
|
|
|
|
chatID := uuid.New()
|
|
ownerID := uuid.New()
|
|
workspaceID := uuid.New()
|
|
orgID := uuid.New()
|
|
fileID := uuid.New()
|
|
chatSnapshot := database.Chat{
|
|
ID: chatID,
|
|
OwnerID: ownerID,
|
|
WorkspaceID: uuid.NullUUID{UUID: workspaceID, Valid: true},
|
|
}
|
|
|
|
expectStoreChatAttachmentTx(t, db, tx)
|
|
tx.EXPECT().GetWorkspaceByID(gomock.Any(), workspaceID).Return(database.Workspace{ID: workspaceID, OrganizationID: orgID}, nil)
|
|
tx.EXPECT().InsertChatFile(gomock.Any(), gomock.Any()).Return(database.InsertChatFileRow{ID: fileID}, nil)
|
|
tx.EXPECT().LinkChatFiles(gomock.Any(), database.LinkChatFilesParams{
|
|
ChatID: chatID,
|
|
MaxFileLinks: int32(codersdk.MaxChatFileIDs),
|
|
FileIds: []uuid.UUID{fileID},
|
|
}).Return(int32(0), context.DeadlineExceeded)
|
|
|
|
attachment, err := server.storeChatAttachment(context.Background(), chatSnapshot, "build.log", "build.log", []byte("build output"))
|
|
require.ErrorContains(t, err, "link chat file")
|
|
require.ErrorIs(t, err, context.DeadlineExceeded)
|
|
require.Equal(t, chattool.AttachmentMetadata{}, attachment)
|
|
}
|
|
|
|
func expectStoreChatAttachmentTx(t *testing.T, db, tx *dbmock.MockStore) {
|
|
t.Helper()
|
|
|
|
db.EXPECT().InTx(gomock.Any(), gomock.AssignableToTypeOf(&database.TxOptions{})).DoAndReturn(
|
|
func(fn func(database.Store) error, opts *database.TxOptions) error {
|
|
require.NotNil(t, opts)
|
|
require.Equal(t, "store_chat_attachment", opts.TxIdentifier)
|
|
return fn(tx)
|
|
},
|
|
)
|
|
}
|