mirror of
https://github.com/coder/coder.git
synced 2026-09-24 15:04:27 +08:00
feat: fall back to the Everyone group for AI spend attribution (#27364)
## Description Previously, a user with no per-user override and no membership in a budgeted group had no effective group, so their AI spend was attributed nowhere and was, therefore, untracked. This change falls back to the organization's Everyone group when no override or group budget applies. Since every user in an organization is implicitly a member of that org's Everyone group, spend is now attributed and tracked for any user with organization membership. A user with no organization membership resolves to no group, so their daily spend is not incremented and a warning is logged. The fallback is unlimited, so enforcement is unaffected: only override and group budgets can block requests. For users in multiple organizations, an existing budget on any Everyone group is still chosen by the "highest" policy; when none is budgeted, the fallback prefers the default org, then orders by organization name. ## Changes - Add `ResolveUserEffectiveGroup` and the `GetUserEveryoneFallbackGroup` query: resolve override → group budget → Everyone group fallback. - Attribute token-usage spend and the user AI spend endpoint via the fallback, so unbudgeted users resolve to their Everyone group instead of null. - Update `GetGroupMembersAISpend` to surface the Everyone fallback as the effective group. - Update `GetHighestGroupAIBudgetByUser` to break ties by organization name then group name, keeping multi-org resolution deterministic and consistent with the fallback. - For multi-org users with no budget anywhere, the fallback picks the Everyone group deterministically: prefer the default org, then order by organization name. Closes https://linear.app/codercom/issue/AIGOV-509/fall-back-to-the-everyone-group-for-spend-attribution > [!NOTE] > Initially generated by Claude Opus 4.7, modified and reviewed by @ssncferreira
This commit is contained in:
@@ -13359,8 +13359,8 @@ func TestGetGroupMembersAISpend(t *testing.T) {
|
||||
user := dbgen.User(t, db, database.User{})
|
||||
org := dbgen.Organization(t, db, database.Organization{})
|
||||
queried := dbgen.Group(t, db, database.Group{OrganizationID: org.ID})
|
||||
groupA := dbgen.Group(t, db, database.Group{OrganizationID: org.ID, Name: "aaa-tie-group"})
|
||||
groupB := dbgen.Group(t, db, database.Group{OrganizationID: org.ID, Name: "bbb-tie-group"})
|
||||
groupA := dbgen.Group(t, db, database.Group{OrganizationID: org.ID})
|
||||
groupB := dbgen.Group(t, db, database.Group{OrganizationID: org.ID})
|
||||
dbgen.OrganizationMember(t, db, database.OrganizationMember{UserID: user.ID, OrganizationID: org.ID})
|
||||
dbgen.GroupMember(t, db, database.GroupMemberTable{GroupID: queried.ID, UserID: user.ID})
|
||||
dbgen.GroupMember(t, db, database.GroupMemberTable{GroupID: groupA.ID, UserID: user.ID})
|
||||
@@ -13376,6 +13376,14 @@ func TestGetGroupMembersAISpend(t *testing.T) {
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
// Both groups are in the same org, so both resolve to the same
|
||||
// organization membership and the tie falls to the lowest group ID.
|
||||
winner := groupA.ID
|
||||
// Postgres orders the uuid type by its bytes.
|
||||
if bytes.Compare(groupB.ID[:], groupA.ID[:]) < 0 {
|
||||
winner = groupB.ID
|
||||
}
|
||||
|
||||
// When: querying spend for the user.
|
||||
got, err := db.GetGroupMembersAISpend(ctx, database.GetGroupMembersAISpendParams{
|
||||
GroupID: queried.ID,
|
||||
@@ -13384,9 +13392,9 @@ func TestGetGroupMembersAISpend(t *testing.T) {
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
// Then: the tie is broken by group name ascending, so groupA wins.
|
||||
// Then: the tie falls to the lowest group ID.
|
||||
require.Len(t, got, 1)
|
||||
require.Equal(t, uuid.NullUUID{UUID: groupA.ID, Valid: true}, got[0].EffectiveGroupID)
|
||||
require.Equal(t, uuid.NullUUID{UUID: winner, Valid: true}, got[0].EffectiveGroupID)
|
||||
require.False(t, got[0].SpendLimitMicros.Valid)
|
||||
require.False(t, got[0].LimitSource.Valid)
|
||||
require.Equal(t, int64(0), got[0].GroupSpendMicros)
|
||||
@@ -13431,6 +13439,89 @@ func TestGetGroupMembersAISpend(t *testing.T) {
|
||||
require.Equal(t, int64(0), got[0].GroupSpendMicros)
|
||||
})
|
||||
|
||||
t.Run("FallbackToEveryoneGroup", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
db, _ := dbtestutil.NewDB(t)
|
||||
ctx := testutil.Context(t, testutil.WaitShort)
|
||||
|
||||
// Given: an unbudgeted member of the queried group whose org has an
|
||||
// Everyone group but no override or budgeted group.
|
||||
user := dbgen.User(t, db, database.User{})
|
||||
org := dbgen.Organization(t, db, database.Organization{})
|
||||
queried := dbgen.Group(t, db, database.Group{OrganizationID: org.ID})
|
||||
dbgen.OrganizationMember(t, db, database.OrganizationMember{UserID: user.ID, OrganizationID: org.ID})
|
||||
dbgen.GroupMember(t, db, database.GroupMemberTable{GroupID: queried.ID, UserID: user.ID})
|
||||
// The Everyone group (id == org id) must exist for the effective group
|
||||
// join to resolve the fallback.
|
||||
//nolint:gocritic // Requires system context.
|
||||
_, err := db.InsertAllUsersGroup(dbauthz.AsSystemRestricted(ctx), org.ID)
|
||||
require.NoError(t, err)
|
||||
_, err = db.IncrementUserAIDailySpend(ctx, database.IncrementUserAIDailySpendParams{
|
||||
UserID: user.ID, EffectiveGroupID: queried.ID, Day: now, CostMicros: 250,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
// When: querying spend for the user.
|
||||
got, err := db.GetGroupMembersAISpend(ctx, database.GetGroupMembersAISpendParams{
|
||||
GroupID: queried.ID,
|
||||
UserIds: []uuid.UUID{user.ID},
|
||||
PeriodStart: monthStart,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
// Then: with no budget, the effective group falls back to the Everyone
|
||||
// group. The limit and source are null, and queried-group spend is returned.
|
||||
require.Len(t, got, 1)
|
||||
require.Equal(t, uuid.NullUUID{UUID: org.ID, Valid: true}, got[0].EffectiveGroupID)
|
||||
require.False(t, got[0].SpendLimitMicros.Valid)
|
||||
require.False(t, got[0].LimitSource.Valid)
|
||||
require.Equal(t, int64(250), got[0].GroupSpendMicros)
|
||||
})
|
||||
|
||||
t.Run("CrossOrgFallbackMasked", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
db, _ := dbtestutil.NewDB(t)
|
||||
ctx := testutil.Context(t, testutil.WaitShort)
|
||||
|
||||
// Given: an unbudgeted member of the queried group who joined another
|
||||
// org earlier. The fallback picks the earlier org's Everyone group.
|
||||
user := dbgen.User(t, db, database.User{})
|
||||
queriedOrg := dbgen.Organization(t, db, database.Organization{})
|
||||
otherOrg := dbgen.Organization(t, db, database.Organization{})
|
||||
queried := dbgen.Group(t, db, database.Group{OrganizationID: queriedOrg.ID})
|
||||
dbgen.OrganizationMember(t, db, database.OrganizationMember{UserID: user.ID, OrganizationID: otherOrg.ID, CreatedAt: dbtime.Now().Add(-time.Hour)})
|
||||
dbgen.OrganizationMember(t, db, database.OrganizationMember{UserID: user.ID, OrganizationID: queriedOrg.ID})
|
||||
dbgen.GroupMember(t, db, database.GroupMemberTable{GroupID: queried.ID, UserID: user.ID})
|
||||
// Both orgs have an Everyone group (id == org id), as in production.
|
||||
//nolint:gocritic // Requires system context.
|
||||
_, err := db.InsertAllUsersGroup(dbauthz.AsSystemRestricted(ctx), queriedOrg.ID)
|
||||
require.NoError(t, err)
|
||||
//nolint:gocritic // Requires system context.
|
||||
_, err = db.InsertAllUsersGroup(dbauthz.AsSystemRestricted(ctx), otherOrg.ID)
|
||||
require.NoError(t, err)
|
||||
_, err = db.IncrementUserAIDailySpend(ctx, database.IncrementUserAIDailySpendParams{
|
||||
UserID: user.ID, EffectiveGroupID: queried.ID, Day: now, CostMicros: 250,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
// When: querying spend for the user.
|
||||
got, err := db.GetGroupMembersAISpend(ctx, database.GetGroupMembersAISpendParams{
|
||||
GroupID: queried.ID,
|
||||
UserIds: []uuid.UUID{user.ID},
|
||||
PeriodStart: monthStart,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
// Then: the fallback resolves to the other org's Everyone group, so
|
||||
// effective_group_id is masked to null, while queried-group spend still
|
||||
// returns.
|
||||
require.Len(t, got, 1)
|
||||
require.False(t, got[0].EffectiveGroupID.Valid, "cross-org effective group must be masked")
|
||||
require.False(t, got[0].SpendLimitMicros.Valid)
|
||||
require.False(t, got[0].LimitSource.Valid)
|
||||
require.Equal(t, int64(250), got[0].GroupSpendMicros)
|
||||
})
|
||||
|
||||
t.Run("SpendWithDifferentEffectiveGroup", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
db, _ := dbtestutil.NewDB(t)
|
||||
@@ -13686,6 +13777,220 @@ func TestGetGroupMembersAISpend(t *testing.T) {
|
||||
})
|
||||
}
|
||||
|
||||
func TestGetHighestGroupAIBudgetByUser(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
setup func(t *testing.T, ctx context.Context, db database.Store) (userID uuid.UUID, want database.GetHighestGroupAIBudgetByUserRow)
|
||||
wantErr error
|
||||
}{
|
||||
{
|
||||
// Among the user's budgeted groups, the highest limit wins.
|
||||
name: "HighestWins",
|
||||
setup: func(t *testing.T, ctx context.Context, db database.Store) (uuid.UUID, database.GetHighestGroupAIBudgetByUserRow) {
|
||||
user := dbgen.User(t, db, database.User{})
|
||||
org := dbgen.Organization(t, db, database.Organization{})
|
||||
lower := dbgen.Group(t, db, database.Group{OrganizationID: org.ID})
|
||||
higher := dbgen.Group(t, db, database.Group{OrganizationID: org.ID})
|
||||
dbgen.OrganizationMember(t, db, database.OrganizationMember{OrganizationID: org.ID, UserID: user.ID})
|
||||
dbgen.GroupMember(t, db, database.GroupMemberTable{GroupID: lower.ID, UserID: user.ID})
|
||||
dbgen.GroupMember(t, db, database.GroupMemberTable{GroupID: higher.ID, UserID: user.ID})
|
||||
_, err := db.UpsertGroupAIBudget(ctx, database.UpsertGroupAIBudgetParams{GroupID: lower.ID, SpendLimitMicros: 1_000_000})
|
||||
require.NoError(t, err)
|
||||
_, err = db.UpsertGroupAIBudget(ctx, database.UpsertGroupAIBudgetParams{GroupID: higher.ID, SpendLimitMicros: 2_000_000})
|
||||
require.NoError(t, err)
|
||||
return user.ID, database.GetHighestGroupAIBudgetByUserRow{GroupID: higher.ID, SpendLimitMicros: 2_000_000}
|
||||
},
|
||||
},
|
||||
{
|
||||
// The highest limit wins across the user's orgs, not just within one.
|
||||
name: "HighestWinsAcrossOrgs",
|
||||
setup: func(t *testing.T, ctx context.Context, db database.Store) (uuid.UUID, database.GetHighestGroupAIBudgetByUserRow) {
|
||||
user := dbgen.User(t, db, database.User{})
|
||||
orgA := dbgen.Organization(t, db, database.Organization{})
|
||||
orgB := dbgen.Organization(t, db, database.Organization{})
|
||||
lower := dbgen.Group(t, db, database.Group{OrganizationID: orgA.ID})
|
||||
higher := dbgen.Group(t, db, database.Group{OrganizationID: orgB.ID})
|
||||
dbgen.OrganizationMember(t, db, database.OrganizationMember{OrganizationID: orgA.ID, UserID: user.ID})
|
||||
dbgen.OrganizationMember(t, db, database.OrganizationMember{OrganizationID: orgB.ID, UserID: user.ID})
|
||||
dbgen.GroupMember(t, db, database.GroupMemberTable{GroupID: lower.ID, UserID: user.ID})
|
||||
dbgen.GroupMember(t, db, database.GroupMemberTable{GroupID: higher.ID, UserID: user.ID})
|
||||
_, err := db.UpsertGroupAIBudget(ctx, database.UpsertGroupAIBudgetParams{GroupID: lower.ID, SpendLimitMicros: 1_000_000})
|
||||
require.NoError(t, err)
|
||||
_, err = db.UpsertGroupAIBudget(ctx, database.UpsertGroupAIBudgetParams{GroupID: higher.ID, SpendLimitMicros: 2_000_000})
|
||||
require.NoError(t, err)
|
||||
return user.ID, database.GetHighestGroupAIBudgetByUserRow{GroupID: higher.ID, SpendLimitMicros: 2_000_000}
|
||||
},
|
||||
},
|
||||
{
|
||||
// A budgeted group in a soft-deleted org is excluded even when its
|
||||
// limit is higher.
|
||||
name: "ExcludesDeletedOrg",
|
||||
setup: func(t *testing.T, ctx context.Context, db database.Store) (uuid.UUID, database.GetHighestGroupAIBudgetByUserRow) {
|
||||
user := dbgen.User(t, db, database.User{})
|
||||
liveOrg := dbgen.Organization(t, db, database.Organization{Name: "live-org"})
|
||||
deletedOrg := dbgen.Organization(t, db, database.Organization{Name: "deleted-org"})
|
||||
liveGroup := dbgen.Group(t, db, database.Group{OrganizationID: liveOrg.ID})
|
||||
deletedGroup := dbgen.Group(t, db, database.Group{OrganizationID: deletedOrg.ID})
|
||||
dbgen.OrganizationMember(t, db, database.OrganizationMember{OrganizationID: liveOrg.ID, UserID: user.ID})
|
||||
dbgen.OrganizationMember(t, db, database.OrganizationMember{OrganizationID: deletedOrg.ID, UserID: user.ID})
|
||||
dbgen.GroupMember(t, db, database.GroupMemberTable{GroupID: liveGroup.ID, UserID: user.ID})
|
||||
dbgen.GroupMember(t, db, database.GroupMemberTable{GroupID: deletedGroup.ID, UserID: user.ID})
|
||||
_, err := db.UpsertGroupAIBudget(ctx, database.UpsertGroupAIBudgetParams{GroupID: liveGroup.ID, SpendLimitMicros: 1_000_000})
|
||||
require.NoError(t, err)
|
||||
_, err = db.UpsertGroupAIBudget(ctx, database.UpsertGroupAIBudgetParams{GroupID: deletedGroup.ID, SpendLimitMicros: 5_000_000})
|
||||
require.NoError(t, err)
|
||||
err = db.UpdateOrganizationDeletedByID(ctx, database.UpdateOrganizationDeletedByIDParams{
|
||||
ID: deletedOrg.ID,
|
||||
UpdatedAt: dbtime.Now(),
|
||||
})
|
||||
require.NoError(t, err)
|
||||
return user.ID, database.GetHighestGroupAIBudgetByUserRow{GroupID: liveGroup.ID, SpendLimitMicros: 1_000_000}
|
||||
},
|
||||
},
|
||||
{
|
||||
// Equal limits across orgs break by the earliest organization
|
||||
// membership.
|
||||
name: "TieByEarliestOrgMembership",
|
||||
setup: func(t *testing.T, ctx context.Context, db database.Store) (uuid.UUID, database.GetHighestGroupAIBudgetByUserRow) {
|
||||
user := dbgen.User(t, db, database.User{})
|
||||
earlyOrg := dbgen.Organization(t, db, database.Organization{})
|
||||
lateOrg := dbgen.Organization(t, db, database.Organization{})
|
||||
earlyGroup := dbgen.Group(t, db, database.Group{OrganizationID: earlyOrg.ID})
|
||||
lateGroup := dbgen.Group(t, db, database.Group{OrganizationID: lateOrg.ID})
|
||||
dbgen.OrganizationMember(t, db, database.OrganizationMember{OrganizationID: earlyOrg.ID, UserID: user.ID, CreatedAt: dbtime.Now().Add(-time.Hour)})
|
||||
dbgen.OrganizationMember(t, db, database.OrganizationMember{OrganizationID: lateOrg.ID, UserID: user.ID})
|
||||
dbgen.GroupMember(t, db, database.GroupMemberTable{GroupID: earlyGroup.ID, UserID: user.ID})
|
||||
dbgen.GroupMember(t, db, database.GroupMemberTable{GroupID: lateGroup.ID, UserID: user.ID})
|
||||
_, err := db.UpsertGroupAIBudget(ctx, database.UpsertGroupAIBudgetParams{GroupID: earlyGroup.ID, SpendLimitMicros: 1_000_000})
|
||||
require.NoError(t, err)
|
||||
_, err = db.UpsertGroupAIBudget(ctx, database.UpsertGroupAIBudgetParams{GroupID: lateGroup.ID, SpendLimitMicros: 1_000_000})
|
||||
require.NoError(t, err)
|
||||
return user.ID, database.GetHighestGroupAIBudgetByUserRow{GroupID: earlyGroup.ID, SpendLimitMicros: 1_000_000}
|
||||
},
|
||||
},
|
||||
{
|
||||
// A user with no budgeted group has no highest budget.
|
||||
name: "NoBudgetedGroup",
|
||||
wantErr: sql.ErrNoRows,
|
||||
setup: func(t *testing.T, ctx context.Context, db database.Store) (uuid.UUID, database.GetHighestGroupAIBudgetByUserRow) {
|
||||
user := dbgen.User(t, db, database.User{})
|
||||
org := dbgen.Organization(t, db, database.Organization{})
|
||||
dbgen.OrganizationMember(t, db, database.OrganizationMember{OrganizationID: org.ID, UserID: user.ID})
|
||||
return user.ID, database.GetHighestGroupAIBudgetByUserRow{}
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
db, _ := dbtestutil.NewDB(t)
|
||||
ctx := testutil.Context(t, testutil.WaitShort)
|
||||
|
||||
userID, want := tt.setup(t, ctx, db)
|
||||
got, err := db.GetHighestGroupAIBudgetByUser(ctx, userID)
|
||||
if tt.wantErr != nil {
|
||||
require.ErrorIs(t, err, tt.wantErr)
|
||||
return
|
||||
}
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, want, got)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetUserEveryoneFallbackGroup(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
setup func(t *testing.T, ctx context.Context, db database.Store) (userID uuid.UUID, wantGroupID uuid.UUID)
|
||||
wantErr error
|
||||
}{
|
||||
{
|
||||
// A single-org member falls back to that org's Everyone group.
|
||||
name: "SingleOrg",
|
||||
setup: func(t *testing.T, ctx context.Context, db database.Store) (uuid.UUID, uuid.UUID) {
|
||||
org := dbgen.Organization(t, db, database.Organization{})
|
||||
user := dbgen.User(t, db, database.User{})
|
||||
dbgen.OrganizationMember(t, db, database.OrganizationMember{OrganizationID: org.ID, UserID: user.ID})
|
||||
return user.ID, org.ID
|
||||
},
|
||||
},
|
||||
{
|
||||
// The default org is preferred even over an org joined earlier.
|
||||
name: "PrefersDefaultOrg",
|
||||
setup: func(t *testing.T, ctx context.Context, db database.Store) (uuid.UUID, uuid.UUID) {
|
||||
defaultOrg, err := db.GetDefaultOrganization(ctx)
|
||||
require.NoError(t, err)
|
||||
otherOrg := dbgen.Organization(t, db, database.Organization{})
|
||||
user := dbgen.User(t, db, database.User{})
|
||||
dbgen.OrganizationMember(t, db, database.OrganizationMember{OrganizationID: otherOrg.ID, UserID: user.ID, CreatedAt: dbtime.Now().Add(-time.Hour)})
|
||||
dbgen.OrganizationMember(t, db, database.OrganizationMember{OrganizationID: defaultOrg.ID, UserID: user.ID})
|
||||
return user.ID, defaultOrg.ID
|
||||
},
|
||||
},
|
||||
{
|
||||
// Among non-default orgs, ties break by the earliest organization
|
||||
// membership.
|
||||
name: "TieByEarliestOrgMembership",
|
||||
setup: func(t *testing.T, ctx context.Context, db database.Store) (uuid.UUID, uuid.UUID) {
|
||||
user := dbgen.User(t, db, database.User{})
|
||||
earlyOrg := dbgen.Organization(t, db, database.Organization{})
|
||||
lateOrg := dbgen.Organization(t, db, database.Organization{})
|
||||
dbgen.OrganizationMember(t, db, database.OrganizationMember{OrganizationID: earlyOrg.ID, UserID: user.ID, CreatedAt: dbtime.Now().Add(-time.Hour)})
|
||||
dbgen.OrganizationMember(t, db, database.OrganizationMember{OrganizationID: lateOrg.ID, UserID: user.ID})
|
||||
return user.ID, earlyOrg.ID
|
||||
},
|
||||
},
|
||||
{
|
||||
// A soft-deleted org is excluded even when it was joined earlier.
|
||||
name: "ExcludesDeletedOrg",
|
||||
setup: func(t *testing.T, ctx context.Context, db database.Store) (uuid.UUID, uuid.UUID) {
|
||||
user := dbgen.User(t, db, database.User{})
|
||||
liveOrg := dbgen.Organization(t, db, database.Organization{Name: "live-org"})
|
||||
deletedOrg := dbgen.Organization(t, db, database.Organization{Name: "deleted-org"})
|
||||
dbgen.OrganizationMember(t, db, database.OrganizationMember{OrganizationID: liveOrg.ID, UserID: user.ID})
|
||||
dbgen.OrganizationMember(t, db, database.OrganizationMember{OrganizationID: deletedOrg.ID, UserID: user.ID, CreatedAt: dbtime.Now().Add(-time.Hour)})
|
||||
err := db.UpdateOrganizationDeletedByID(ctx, database.UpdateOrganizationDeletedByIDParams{
|
||||
ID: deletedOrg.ID,
|
||||
UpdatedAt: dbtime.Now(),
|
||||
})
|
||||
require.NoError(t, err)
|
||||
return user.ID, liveOrg.ID
|
||||
},
|
||||
},
|
||||
{
|
||||
// A user with no org membership has no fallback group.
|
||||
name: "NoOrgMembership",
|
||||
wantErr: sql.ErrNoRows,
|
||||
setup: func(t *testing.T, ctx context.Context, db database.Store) (uuid.UUID, uuid.UUID) {
|
||||
user := dbgen.User(t, db, database.User{})
|
||||
return user.ID, uuid.Nil
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
db, _ := dbtestutil.NewDB(t)
|
||||
ctx := testutil.Context(t, testutil.WaitShort)
|
||||
|
||||
userID, wantGroupID := tt.setup(t, ctx, db)
|
||||
got, err := db.GetUserEveryoneFallbackGroup(ctx, userID)
|
||||
if tt.wantErr != nil {
|
||||
require.ErrorIs(t, err, tt.wantErr)
|
||||
return
|
||||
}
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, wantGroupID, got)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestChatPinOrderQueries(t *testing.T) {
|
||||
t.Parallel()
|
||||
if testing.Short() {
|
||||
|
||||
Reference in New Issue
Block a user