Order organizations of one name by id in the list by type
ListOrganizationsByType ordered by name alone, and organization names are not unique, so two organizations of one name came back in whatever order the sort met them (reported on #187). The query now breaks the tie on org_id. The test inserts six organizations of one name in descending id order, so it fails under the old order. No golden output moves: the fixtures' organization names are distinct, and the full suite passes unchanged.
This commit is contained in:
@@ -289,7 +289,7 @@ func (q *Queries) ListOrganizations(ctx context.Context) ([]Organization, error)
|
||||
const listOrganizationsByType = `-- name: ListOrganizationsByType :many
|
||||
SELECT org_id, name, key, org_type, owner_person_id, status, created_at, updated_at FROM core.organizations
|
||||
WHERE org_type = $1
|
||||
ORDER BY name
|
||||
ORDER BY name, org_id
|
||||
`
|
||||
|
||||
func (q *Queries) ListOrganizationsByType(ctx context.Context, orgType string) ([]Organization, error) {
|
||||
|
||||
@@ -0,0 +1,70 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Commercial
|
||||
// SPDX-FileCopyrightText: 2025-2026 Christian Galo
|
||||
|
||||
package organization_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"slices"
|
||||
"testing"
|
||||
|
||||
"github.com/google/uuid"
|
||||
|
||||
"git.coopcloud.tech/wiki-cafe/member-console/internal/identity"
|
||||
"git.coopcloud.tech/wiki-cafe/member-console/internal/organization"
|
||||
"git.coopcloud.tech/wiki-cafe/member-console/internal/testkit/dbtest"
|
||||
)
|
||||
|
||||
// Scenario: organization names are not unique, so ListOrganizationsByType
|
||||
// breaks a tie on the name by org_id and organizations of one name come
|
||||
// back in the same order every time.
|
||||
func TestListOrganizationsByTypeOrdersEqualNamesByID(t *testing.T) {
|
||||
database := dbtest.Open(t)
|
||||
ctx := context.Background()
|
||||
iq := identity.New(database)
|
||||
oq := organization.New(database)
|
||||
|
||||
user, err := iq.CreateUser(ctx, "order-"+uuid.NewString())
|
||||
if err != nil {
|
||||
t.Fatalf("user: %v", err)
|
||||
}
|
||||
person, err := iq.CreatePerson(ctx, identity.CreatePersonParams{
|
||||
UserID: user.UserID,
|
||||
DisplayName: "Order",
|
||||
PrimaryEmail: "order-" + uuid.NewString()[:8] + "@example.com",
|
||||
PrimaryEmailVerified: true,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("person: %v", err)
|
||||
}
|
||||
name := "Twin " + uuid.NewString()[:8]
|
||||
// Insert the ids in descending order: uuidv7 defaults are time-ordered, so
|
||||
// default ids would arrive ascending and hide a missing tie-breaker. Several
|
||||
// rows keep free-space reuse in the heap from reordering them all by luck.
|
||||
want := make([]string, 6)
|
||||
for i := range want {
|
||||
want[i] = uuid.NewString()
|
||||
}
|
||||
slices.Sort(want)
|
||||
for _, id := range slices.Backward(want) {
|
||||
if _, err := database.ExecContext(ctx,
|
||||
`INSERT INTO core.organizations (org_id, name, org_type, owner_person_id) VALUES ($1, $2, 'personal', $3)`,
|
||||
id, name, person.PersonID); err != nil {
|
||||
t.Fatalf("org: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
orgs, err := oq.ListOrganizationsByType(ctx, "personal")
|
||||
if err != nil {
|
||||
t.Fatalf("list: %v", err)
|
||||
}
|
||||
var ids []string
|
||||
for _, org := range orgs {
|
||||
if org.Name == name {
|
||||
ids = append(ids, org.OrgID)
|
||||
}
|
||||
}
|
||||
if !slices.Equal(ids, want) {
|
||||
t.Errorf("organizations of one name = %v, want ascending org_id %v", ids, want)
|
||||
}
|
||||
}
|
||||
@@ -87,7 +87,7 @@ LIMIT sqlc.arg(row_limit)::int;
|
||||
-- name: ListOrganizationsByType :many
|
||||
SELECT * FROM core.organizations
|
||||
WHERE org_type = $1
|
||||
ORDER BY name;
|
||||
ORDER BY name, org_id;
|
||||
|
||||
-- name: CountTeamOrganizations :one
|
||||
-- Operator overview headline. Personal orgs exist one-per-member by
|
||||
|
||||
@@ -23,7 +23,7 @@ Each numbered section below is one commit, and its items are that commit's parts
|
||||
|
||||
## 2. Ride-along fixes that stand alone
|
||||
|
||||
- [ ] 2.1 End `ListOrganizationsByType` (`internal/organization/queries/organizations.sql`) with `ORDER BY name, org_id`, regenerate sqlc, and add a test with two organizations of one name that pins their order. No golden moves (the fixtures' names are distinct); the commit says so after running the suite.
|
||||
- [x] 2.1 End `ListOrganizationsByType` (`internal/organization/queries/organizations.sql`) with `ORDER BY name, org_id`, regenerate sqlc, and add a test with two organizations of one name that pins their order. No golden moves (the fixtures' names are distinct); the commit says so after running the suite.
|
||||
- [ ] 2.2 `PollEntitlementRecompute` (`internal/workflows/entitlements/recompute_drain_workflow.go`) logs a failed `ListChangesOwingRecomputeActivity` and goes on to its sleep and continue-as-new instead of returning the error, because Temporal's five retries end after about fifteen seconds and nothing restarts a failed poller. Test with the Temporal test suite: the list activity fails on every attempt, and the poller ends with a continue-as-new error, not the activity's.
|
||||
- [ ] 2.3 The `entitlement-set-history` delta corrects the drain-selection sentence to the dead-letter behaviour. Add a database test beside `TestSettleObligationOnSettledObligationWritesNothing` that a `failed` obligation is not returned by `ListPendingObligations` and is returned after `RequeueFailedObligations`.
|
||||
- [ ] 2.4 Change `MessageInProgress` (`internal/entitlements/rule_change.go:93`) to "Another change collided with this one; try again." and the literal in `internal/entitlements/rule_change_test.go:490`; the other tests compare the constant. The `entitlements` and `entitlement-set-management` deltas carry the sentence. No golden moves: the plan-act goldens' copy of the old sentence comes from the plan-act handlers, which change in tasks 4 to 8.
|
||||
|
||||
Reference in New Issue
Block a user