Files
multica/server/internal/integrations/dingtalk/install_test.go
YYClaw c3577cb04b feat(dingtalk): add DingTalk bot integration (MUL-3958) (#4829)
Adds a DingTalk (钉钉) bot integration on the bring-your-own-app model: a
workspace admin creates their own Stream-mode robot and pastes its AppKey /
AppSecret, so no public webhook or OAuth redirect is required. Each agent gets
its own bot identity, so several agents can be distinct, separately
@-mentionable contacts in one DingTalk organization.

Supports DMs, @-mentions in groups, inbound images, /issue quick-create, and
/new. Built on the shared channel engine (ForceFresh/BareFresh, MediaResolver /
MediaRef and the intent ledger) rather than a private implementation.

Off unless MULTICA_DINGTALK_SECRET_KEY is set. Docs in en/zh/ja/ko.

Closes #4791.

Community-maintained: @yyclaw is the code owner for
server/internal/integrations/dingtalk/.
2026-08-06 15:36:02 +08:00

165 lines
5.6 KiB
Go

package dingtalk
import (
"context"
"testing"
"github.com/jackc/pgx/v5"
"github.com/jackc/pgx/v5/pgconn"
"github.com/jackc/pgx/v5/pgtype"
"github.com/multica-ai/multica/server/internal/util"
"github.com/multica-ai/multica/server/internal/util/secretbox"
db "github.com/multica-ai/multica/server/pkg/db/generated"
)
func testBox(t *testing.T) *secretbox.Box {
t.Helper()
key := make([]byte, secretbox.KeySize)
for i := range key {
key[i] = byte(i + 1)
}
box, err := secretbox.New(key)
if err != nil {
t.Fatalf("secretbox.New: %v", err)
}
return box
}
func mustUUID(t *testing.T, s string) pgtype.UUID {
t.Helper()
u, err := util.ParseUUID(s)
if err != nil {
t.Fatalf("parse uuid %q: %v", s, err)
}
return u
}
type fakeInstallQueries struct {
// existing, when set, is the agent's current row; UpsertChannelInstallation
// returns it (an UPDATE) so a reconnect reuses the same row id.
existing *db.ChannelInstallation
// existingAppID is the current row's platform identity returned under lock.
// It distinguishes a same-robot reconnect from a different-robot replacement.
existingAppID string
lockCalled bool
replaceCalled bool
replaceParams db.DeleteDingTalkInstallationForReplacementParams
// appIDTaken makes UpsertChannelInstallation report a unique-constraint
// violation on the (channel_type, app_id) routing index — i.e. the pasted
// robot is already connected to a LIVE owner (the reclaim has run by then).
appIDTaken bool
upsertParams db.UpsertChannelInstallationParams
upsertCalled bool
rowID pgtype.UUID
// reclaimedID, when set, is returned by ReclaimDeadChannelInstallationByAppID
// to model a dead prior owner having been cleared; otherwise it reports
// pgx.ErrNoRows (nothing was dead). reclaimCalled records that the install
// path ran the reclaim before upserting.
reclaimedID *pgtype.UUID
reclaimCalled bool
// ownerWorkspaceID / ownerArchived / ownerMissing drive the live-owner lookup
// that classifies an appIDTaken conflict into the right sentinel.
ownerWorkspaceID pgtype.UUID
ownerArchived bool
ownerMissing bool
}
// WithTx returns the same fake — the fake tx is a no-op token.
func (f *fakeInstallQueries) WithTx(_ pgx.Tx) installQueries { return f }
func (f *fakeInstallQueries) LockDingTalkInstallationOwner(_ context.Context, _ db.LockDingTalkInstallationOwnerParams) error {
f.lockCalled = true
return nil
}
func (f *fakeInstallQueries) GetDingTalkInstallationOwnerForUpdate(_ context.Context, _ db.GetDingTalkInstallationOwnerForUpdateParams) (db.GetDingTalkInstallationOwnerForUpdateRow, error) {
if f.existing == nil {
return db.GetDingTalkInstallationOwnerForUpdateRow{}, pgx.ErrNoRows
}
return db.GetDingTalkInstallationOwnerForUpdateRow{ID: f.existing.ID, AppID: f.existingAppID}, nil
}
func (f *fakeInstallQueries) DeleteDingTalkInstallationForReplacement(_ context.Context, arg db.DeleteDingTalkInstallationForReplacementParams) (pgtype.UUID, error) {
f.replaceCalled = true
f.replaceParams = arg
id := f.existing.ID
f.existing = nil
return id, nil
}
func (f *fakeInstallQueries) ReclaimDeadChannelInstallationByAppID(_ context.Context, _ db.ReclaimDeadChannelInstallationByAppIDParams) (pgtype.UUID, error) {
f.reclaimCalled = true
if f.reclaimedID != nil {
return *f.reclaimedID, nil
}
return pgtype.UUID{}, pgx.ErrNoRows
}
func (f *fakeInstallQueries) GetChannelInstallationOwnerByAppID(_ context.Context, _ db.GetChannelInstallationOwnerByAppIDParams) (db.GetChannelInstallationOwnerByAppIDRow, error) {
if f.ownerMissing {
return db.GetChannelInstallationOwnerByAppIDRow{}, pgx.ErrNoRows
}
return db.GetChannelInstallationOwnerByAppIDRow{
WorkspaceID: f.ownerWorkspaceID,
AgentArchivedAt: pgtype.Timestamptz{Valid: f.ownerArchived},
}, nil
}
func (f *fakeInstallQueries) UpsertChannelInstallation(_ context.Context, arg db.UpsertChannelInstallationParams) (db.ChannelInstallation, error) {
f.upsertCalled = true
f.upsertParams = arg
if f.appIDTaken {
return db.ChannelInstallation{}, &pgconn.PgError{Code: "23505"}
}
id := f.rowID
if f.existing != nil {
id = f.existing.ID // reconnect updates the agent's existing row in place
}
return db.ChannelInstallation{
ID: id,
WorkspaceID: arg.WorkspaceID,
AgentID: arg.AgentID,
ChannelType: arg.ChannelType,
Config: arg.Config,
InstallerUserID: arg.InstallerUserID,
Status: "active",
}, nil
}
func (f *fakeInstallQueries) ListChannelInstallationsByWorkspace(_ context.Context, _ db.ListChannelInstallationsByWorkspaceParams) ([]db.ChannelInstallation, error) {
return nil, nil
}
func (f *fakeInstallQueries) GetChannelInstallationInWorkspace(_ context.Context, _ db.GetChannelInstallationInWorkspaceParams) (db.ChannelInstallation, error) {
return db.ChannelInstallation{}, nil
}
func (f *fakeInstallQueries) SetChannelInstallationStatus(_ context.Context, _ db.SetChannelInstallationStatusParams) error {
return nil
}
// fakeTx is a no-op pgx.Tx: embedding the interface satisfies it, and the
// install paths only ever call Commit / Rollback.
type fakeTx struct {
pgx.Tx
committed bool
}
func (t *fakeTx) Commit(context.Context) error { t.committed = true; return nil }
func (t *fakeTx) Rollback(context.Context) error { return nil }
type fakeTxStarter struct{ tx *fakeTx }
func (f *fakeTxStarter) Begin(context.Context) (pgx.Tx, error) { return f.tx, nil }
func newTestInstallService(t *testing.T, q installQueries) *InstallService {
t.Helper()
svc, err := newInstallService(q, &fakeTxStarter{tx: &fakeTx{}}, testBox(t), nil)
if err != nil {
t.Fatalf("newInstallService: %v", err)
}
return svc
}