diff --git a/gonsole/auth/postgres/CHANGELOG.md b/gonsole/auth/postgres/CHANGELOG.md index 9d32b3e..e37e77e 100644 --- a/gonsole/auth/postgres/CHANGELOG.md +++ b/gonsole/auth/postgres/CHANGELOG.md @@ -8,6 +8,16 @@ v0.x, minor releases may contain breaking changes. Releases of this module are tagged `gonsole/auth/postgres/vX.Y.Z`. +## [Unreleased] + +### Changed + +- **Breaking.** Requires `gonsole/auth` 0.8.0. + +### Fixed + +- The record store answers each record's time in UTC, so `account:records -json` prints a UTC time, as on SQLite. + ## [0.1.0] - 2026-10-12 ### Added diff --git a/gonsole/auth/postgres/authorize_test.go b/gonsole/auth/postgres/authorize_test.go new file mode 100644 index 0000000..cccd8d2 --- /dev/null +++ b/gonsole/auth/postgres/authorize_test.go @@ -0,0 +1,61 @@ +// SPDX-License-Identifier: Apache-2.0 + +package postgres_test + +import ( + "testing" + + "github.com/gopherium/framework/gonsole" + "github.com/gopherium/framework/gonsole/testkit" +) + +func TestAuthorizeRefusesAnAccountItCannotLetThrough(t *testing.T) { + t.Parallel() + + for _, tc := range []struct { + name, change, actor, stderr string + }{ + {"a role that lacks the capability", "", "editor@example.com", + "myapp: the account editor@example.com holds the role editor, which lacks manage_users\n"}, + {"a disabled account", "UPDATE auth.users SET disabled = true WHERE email = 'admin@example.com'", + "admin@example.com", "myapp: the account admin@example.com is disabled\n"}, + {"an account never activated", "UPDATE auth.users SET confirmed = false WHERE email = 'admin@example.com'", + "admin@example.com", "myapp: the account admin@example.com was never activated\n"}, + {"an account holding no role", "UPDATE auth.users SET role = '' WHERE email = 'author@example.com'", + "author@example.com", "myapp: the account author@example.com holds no role, so it lacks manage_users\n"}, + } { + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + db := seeded(t) + if tc.change != "" { + db.run(tc.change) + } + + got := testkit.Run(t, moduleProgram(db), "", "account:role", "author@example.com", "editor", "-yes", + "-as", tc.actor) + + if got.Code != gonsole.ExitFailed || got.Stderr != tc.stderr { + t.Errorf("account:role as %s = %d with %q, want %d with %q", tc.actor, got.Code, got.Stderr, + gonsole.ExitFailed, tc.stderr) + } + if !db.found("SELECT count(*) = 0 FROM gonsole.records") { + t.Errorf("account:role as %s left a record, want none", tc.actor) + } + }) + } +} + +func TestAuthorizeLetsACapableAccountThrough(t *testing.T) { + t.Parallel() + + db := seeded(t) + + got := testkit.Run(t, moduleProgram(db), "", "account:role", "author@example.com", "editor", "-yes", + "-as", "admin@example.com") + + role := db.text("SELECT role FROM auth.users WHERE email = 'author@example.com'") + if got.Code != gonsole.ExitDone || role != "editor" { + t.Errorf("account:role as the admin = %d with %q and the role %q, want %d and editor", got.Code, got.Stderr, + role, gonsole.ExitDone) + } +} diff --git a/gonsole/auth/postgres/go.mod b/gonsole/auth/postgres/go.mod index c86f8c3..3a65104 100644 --- a/gonsole/auth/postgres/go.mod +++ b/gonsole/auth/postgres/go.mod @@ -6,7 +6,7 @@ require ( github.com/gopherium/framework/dbkit v0.1.0 github.com/gopherium/framework/dbkit/postgres v0.3.0 github.com/gopherium/framework/gonsole v0.6.1 - github.com/gopherium/framework/gonsole/auth v0.7.0 + github.com/gopherium/framework/gonsole/auth v0.8.0 github.com/gopherium/gouncer v0.4.0 github.com/gopherium/gouncer/authkit/postgres v0.13.0 github.com/jackc/pgx/v5 v5.10.0 diff --git a/gonsole/auth/postgres/go.sum b/gonsole/auth/postgres/go.sum index a884cda..55989b2 100644 --- a/gonsole/auth/postgres/go.sum +++ b/gonsole/auth/postgres/go.sum @@ -13,8 +13,8 @@ github.com/gopherium/framework/dbkit/postgres v0.3.0 h1:W9SoXXu9Y3YRum5vIG10iBHl github.com/gopherium/framework/dbkit/postgres v0.3.0/go.mod h1:ZUsvp2lOkhX+08sstBErt+jN6FM5HkhxV7hXgToRd+A= github.com/gopherium/framework/gonsole v0.6.1 h1:yi0te8BDJLuotbrd24bQDMWSJ0KgIhSIAfOjF0jpDnQ= github.com/gopherium/framework/gonsole v0.6.1/go.mod h1:n0aJzbwPGPryrWKZ8WzDVub+6R+GUBzeHSzvhMvZR64= -github.com/gopherium/framework/gonsole/auth v0.7.0 h1:e1s0LJlLK2zPmrzexzMix7pkiTIXqRe0bd83MzYsRiE= -github.com/gopherium/framework/gonsole/auth v0.7.0/go.mod h1:t9PxTMk18NqcoXgxokdUQnOvNPYfcW2HXVjx8fPxei0= +github.com/gopherium/framework/gonsole/auth v0.8.0 h1:WddQbi5eRQs98tTR8qx2bLg8clDeW99Kz6mM1T03pvQ= +github.com/gopherium/framework/gonsole/auth v0.8.0/go.mod h1:Lms8wUhRpxvQd3M8kv1tpMS9Pox0Qh/M+1j4HjLq8PE= github.com/gopherium/gouncer v0.4.0 h1:dkbCD5oeYuuzR961UKGsAvnTvF8vTstlFbN94rP4TvI= github.com/gopherium/gouncer v0.4.0/go.mod h1:WwPOu8LyRikot0EODySSu46zaHcWTPbl5nOes4I3H2U= github.com/gopherium/gouncer/authkit v0.19.0 h1:4TPPakAHNmqG0H+7c62YomNgDfd7uRbuccIXvbdutKc= diff --git a/gonsole/auth/postgres/lineage_test.go b/gonsole/auth/postgres/lineage_test.go deleted file mode 100644 index 3d9efbb..0000000 --- a/gonsole/auth/postgres/lineage_test.go +++ /dev/null @@ -1,67 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 - -package postgres_test - -import ( - "context" - "testing" - - "github.com/gopherium/framework/gonsole" - "github.com/gopherium/framework/gonsole/auth" -) - -const ( - // versionsLookup lists the applied versions of both version tables on one line. - versionsLookup = "SELECT (SELECT string_agg(version_id || ':' || is_applied, ',' ORDER BY id) " + - "FROM auth.goose_db_version) || ' ' || (SELECT string_agg(version_id || ':' || is_applied, ',' ORDER BY id) " + - "FROM gonsole.goose_db_version)" - // plantRecord stores one record past every record store. - plantRecord = "INSERT INTO gonsole.records (id, actor, command, args, flags) VALUES " + - "('019a0000-0000-7000-8000-000000000001', 'admin@example.com', 'account:disable', '[]', '{}')" - // oneRecord answers whether gonsole.records holds exactly one row. - oneRecord = "SELECT count(*) = 1 FROM gonsole.records" -) - -// text returns the single text lookup answers on db. -func (db testDB) text(lookup string) string { - db.t.Helper() - conn := db.dial() - defer func() { _ = conn.Close(context.Background()) }() - var value string - if err := conn.QueryRow(db.t.Context(), lookup).Scan(&value); err != nil { - db.t.Fatalf("%s: %v", lookup, err) - } - return value -} - -func TestAProgramThatSwitchesStepsAppliesNothingTwice(t *testing.T) { - t.Parallel() - - core := []gonsole.Step{auth.Migration(), auth.RecordMigration()} - for _, tc := range []struct { - name string - first []gonsole.Step - second []gonsole.Step - }{ - {"from gonsole/auth to the module", core, steps()}, - {"from the module to gonsole/auth", steps(), core}, - } { - t.Run(tc.name, func(t *testing.T) { - t.Parallel() - - db := empty(t) - db.apply(tc.first...) - db.run(plantRecord) - before := db.text(versionsLookup) - - db.apply(tc.second...) - - if after := db.text(versionsLookup); after != before { - t.Errorf("the version tables read %q after the other steps, want %q as before", after, before) - } - if !db.found(oneRecord) { - t.Error("the planted record is gone after the other steps, want it kept") - } - }) - } -} diff --git a/gonsole/auth/postgres/parity_test.go b/gonsole/auth/postgres/parity_test.go deleted file mode 100644 index a8766f4..0000000 --- a/gonsole/auth/postgres/parity_test.go +++ /dev/null @@ -1,68 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 - -package postgres_test - -import ( - "regexp" - "testing" - - "github.com/gopherium/framework/gonsole/auth" - "github.com/gopherium/framework/gonsole/testkit" -) - -var ( - // stamps matches the times a line prints. - stamps = regexp.MustCompile(`\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(\.\d+)?(Z|[+-]\d{2}:\d{2})`) - // ids matches the UUIDs a line prints. - ids = regexp.MustCompile(`[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}`) -) - -// answer is what one line answered, its times and ids cut. -type answer struct { - // code is the exit code. - code int - // stdout is what the line printed. - stdout string - // stderr is what the line printed as failure. - stderr string -} - -// cut returns the answer of result with each time and id replaced by a placeholder. -func cut(result testkit.Result) answer { - clean := func(text string) string { return ids.ReplaceAllString(stamps.ReplaceAllString(text, "