Skip to content

Conversation

@tolgaozen
Copy link
Member

@tolgaozen tolgaozen commented Sep 3, 2025

Summary by CodeRabbit

  • Documentation

    • API reference metadata bumped to v1.4.4; no endpoint or schema changes.
  • Chores

    • Application/public version bumped to v1.4.4; banners and metadata reflect the update.
  • Tests

    • Large expansion of tests across auth, schema, storage (Postgres), GC, engines, utils, watch, and more — focused on error handling, edge cases, integration, and input validation.
  • Refactor

    • Removed deprecated helpers, inlined/simplified helper logic, and minor whitespace/import cleanups.

@coderabbitai
Copy link

coderabbitai bot commented Sep 3, 2025

Walkthrough

Bumps project/API version metadata from v1.4.3 to v1.4.4, removes a deprecated GC helper, refactors SnapshotQuery to use squirrel expressions, and adds extensive error-path tests across OIDC authn, schema walkers, engines, and many Postgres storage components; includes minor whitespace and helper relocations.

Changes

Cohort / File(s) Summary
Version metadata bump
docs/api-reference/apidocs.swagger.json, docs/api-reference/openapiv2/apidocs.swagger.json, proto/base/v1/openapi.proto, internal/info.go
Update swagger/openapi info.version and internal Version constant from "v1.4.3" to "v1.4.4".
OIDC & preshared authn tests
internal/authn/oidc/adapter_test.go, internal/authn/oidc/authn_test.go, internal/authn/preshared/authn_test.go
Add Ginkgo/Gomega tests for SlogAdapter behavior, OIDC backoff/config/HTTP error paths, context cancellation, and preshared key validation.
Schema error-path tests
internal/schema/linkedSchema_test.go, internal/schema/walker_test.go
Add tests exercising linked-schema and walker error paths (missing entities/relations, undefined kinds, unimplemented leaf types, error propagation).
Postgres utils refactor & tests
internal/storage/postgres/utils/common.go, internal/storage/postgres/utils/common_test.go, internal/storage/postgres/utils/filter_test.go, internal/storage/postgres/utils/version_test.go
Inline/refactor SnapshotQuery to use squirrel expressions; add extensive tests for error mapping, backoff/jitter, logging/tracing, filter builders, and DB version checks.
Postgres readers/writers error tests
internal/storage/postgres/dataReader_test.go, internal/storage/postgres/dataWriter_test.go, internal/storage/postgres/bundleReader_test.go, internal/storage/postgres/schemaReader_test.go, internal/storage/postgres/tenantWriter_test.go
Add comprehensive error-path tests for query/build/execute/scan/serialization, token/snapshot decode, transaction and retry behavior using closed DBs and invalid inputs.
Postgres GC cleanup & tests
internal/storage/postgres/gc/gc.go, internal/storage/postgres/gc/gc_test.go, internal/storage/postgres/gc/options_test.go
Remove deprecated global getLastTransactionID; add GC option unit tests and GC error-path tests (context cancelation, closed DB).
Postgres watch updates
internal/storage/postgres/watch.go, internal/storage/postgres/watch_test.go
Add watch error-path tests (snapshot decode, getChanges, getRecentXIDs) and import internal/storage/postgres/types; remove extra blank lines in getRecentXIDs (whitespace-only).
Postgres misc cleanup/helpers
internal/storage/postgres/consts.go, internal/storage/postgres/postgres_test.go
Remove unused isSameArray from consts.go; add equivalent helper and sort import in postgres_test.go.
Engines tests & lookup
internal/engines/bulk_test.go, internal/engines/expand_test.go, internal/engines/lookup_test.go
Add/extend engine tests covering BulkChecker, Expand, and Lookup error paths, mocks, and validation of context/error handling.

Sequence Diagram(s)

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Suggested reviewers

  • tolgaOzen

Poem

I hopped a patch from three to four, so spry,
Tests blossomed while old helpers said goodbye.
Snapshots stitched and backoffs gently tried,
Closed DBs grumbled, error paths supplied.
A rabbit noses v1.4.4 — whoosh! I sigh 🐰✨

Warning

There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure.

🔧 golangci-lint (2.2.2)

Error: can't load config: unsupported version of the configuration: "" See https://golangci-lint.run/product/migration-guide for migration instructions
The command is terminated due to an error: can't load config: unsupported version of the configuration: "" See https://golangci-lint.run/product/migration-guide for migration instructions

✨ Finishing Touches
  • 📝 Generate Docstrings
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feat-update-permify-api-v1.4.4

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share
🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.

Support

Need help? Create a ticket on our support page for assistance with any issues or questions.

CodeRabbit Commands (Invoked using PR/Issue comments)

Type @coderabbitai help to get the list of available commands.

Other keywords and placeholders

  • Add @coderabbitai ignore or @coderabbit ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

CodeRabbit Configuration File (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • Please see the configuration documentation for more information.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Status, Documentation and Community

  • Visit our Status Page to check the current availability of CodeRabbit.
  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

@tolgaozen tolgaozen changed the title Feat update permify api v1.4.4 feat: update permify api v1.4.4 Sep 3, 2025
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 11

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (3)
internal/storage/postgres/utils/filter_test.go (2)

179-184: Also assert args ordering for UPDATE; it’s as important as SQL shape.

Validating args prevents regressions in placeholder binding order.

-			sql, _, err := sl.ToSql()
+			expectedArgs := []interface{}{1000, "1", "2", "organization", "admin", "4", "8", "user"}
+			sql, args, err := sl.ToSql()
 			Expect(err).ShouldNot(HaveOccurred())
+			Expect(args).Should(Equal(expectedArgs))
 			Expect(sql).Should(Equal(expectedSql))

230-235: Add args assertion for attributes UPDATE with filters.

-			sql, _, err := sl.ToSql()
+			expectedArgs := []interface{}{1000, "public", "balance", "1", "2", "organization"}
+			sql, args, err := sl.ToSql()
 			Expect(err).ShouldNot(HaveOccurred())
+			Expect(args).Should(Equal(expectedArgs))
 			Expect(sql).Should(Equal(expectedSql))
internal/authn/oidc/authn_test.go (1)

520-521: Fix Gomega expectation misuse

Comparing error to the matcher is incorrect; assert occurrence instead.

-Expect(err).ShouldNot(Equal(BeNil()))
+Expect(err).Should(HaveOccurred())
🧹 Nitpick comments (36)
internal/authn/preshared/authn_test.go (1)

41-44: Make assertions less brittle and assert returned authn is nil.

Avoid exact string comparison on error messages and also verify the returned authenticator is nil for stronger guarantees.

Apply:

-               _, err := NewKeyAuthn(context.Background(), emptyConfig)
-               Expect(err).To(HaveOccurred())
-               Expect(err.Error()).To(Equal("pre shared key authn must have at least one key"))
+               auth, err := NewKeyAuthn(context.Background(), emptyConfig)
+               Expect(err).To(HaveOccurred())
+               Expect(auth).To(BeNil())
+               Expect(err).To(MatchError(ContainSubstring("at least one key")))

-               _, err := NewKeyAuthn(context.Background(), nilConfig)
-               Expect(err).To(HaveOccurred())
-               Expect(err.Error()).To(Equal("pre shared key authn must have at least one key"))
+               auth, err := NewKeyAuthn(context.Background(), nilConfig)
+               Expect(err).To(HaveOccurred())
+               Expect(auth).To(BeNil())
+               Expect(err).To(MatchError(ContainSubstring("at least one key")))

Also applies to: 49-53

internal/storage/postgres/postgres_test.go (2)

4-4: Remove sort import if helper is deleted

If you switch to ConsistOf, drop the now-unused import.

-import (
-	"sort"
-	"testing"
+import (
+	"testing"

16-37: Replace isSameArray in postgres tests with Gomega’s ConsistOf and delete the test helper

In internal/storage/postgres/postgres_test.go, remove the local isSameArray helper and update assertions to:

Expect(actual).To(ConsistOf(expected))

Production still defines its own isSameArray in pkg/development/development.go and pkg/cmd/validate.go, so this change is limited to tests. Optionally consolidate the duplicate helpers in those packages into a shared utility.

internal/storage/postgres/utils/filter_test.go (6)

84-98: Re-initialize the builder between assertions to avoid state leakage within the same test.

The second scenario reuses the previously mutated sl. Recreate the builder to keep cases independent.

-			sl = utils.TuplesFilterQueryForSelectBuilder(sl, filter)
+			// re-init builder for the second scenario
+			sl = squirrel.Select("*").From("relation_tuples")
+			sl = utils.TuplesFilterQueryForSelectBuilder(sl, filter)

101-101: Disambiguate duplicate context name.

Two contexts are named “TestEmptyFilterQueryForSelectBuilder”. Rename this one to clarify it targets attributes.

-	Context("TestEmptyFilterQueryForSelectBuilder", func() {
+	Context("TestEmptyAttributeFilterQueryForSelectBuilder", func() {

146-160: Re-initialize update builder before second scenario to prevent state carry-over.

Same pattern here; recreate sl before the second call.

-			sl = utils.TuplesFilterQueryForUpdateBuilder(sl, filter)
+			// re-init builder for the second scenario
+			sl = squirrel.Update("relation_tuples").Set("expired_tx_id", 1000)
+			sl = utils.TuplesFilterQueryForUpdateBuilder(sl, filter)

208-215: Re-initialize attributes update builder before second scenario (consistency).

-			sl = utils.AttributesFilterQueryForUpdateBuilder(sl, filter)
+			// re-init builder for the second scenario
+			sl = squirrel.Update("attributes").Set("expired_tx_id", 1000)
+			sl = utils.AttributesFilterQueryForUpdateBuilder(sl, filter)

454-468: Assert args for single-attribute UPDATE as well.

Keeps UPDATE tests consistent with SELECT tests.

-				sql, _, err := sl.ToSql()
+				expectedArgs := []interface{}{1000, "single_attribute"}
+				sql, args, err := sl.ToSql()
 				Expect(err).ShouldNot(HaveOccurred())
+				Expect(args).Should(Equal(expectedArgs))
 				Expect(sql).Should(Equal(expectedSql))

492-731: Add missing nil-ID/attribute tests and subject_relation-only scenarios

  • Add TuplesFilterQueryForUpdateBuilder tests for nil entity and nil subject IDs (to mirror SELECT nil cases).
  • Add AttributesFilterQueryForUpdateBuilder test for nil attributes (parity with SELECT nil‐attribute case).
  • Add tests covering subject_relation used alone and combined with a single subject ID.
  • Since builders use a multi-key squirrel.Eq map, full-SQL assertions must lock clause order (or refactor builders to sequential Where calls for deterministic ordering).
internal/storage/postgres/utils/version_test.go (3)

3-14: Add strings import (used by env gating in multi-version test below).

Preparing for safer multi-version gating needs strings.

 import (
   "os"
   "strconv"
+  "strings"

37-71: Consolidate redundant assertions into a single spec.

The first four specs repeat the same setup and assertions. Merge for speed and clarity, and add a quick sanity check on idempotency.

-    It("should return version for supported PostgreSQL version", func() {
-      version, err := utils.EnsureDBVersion(writePool)
-      Expect(err).ShouldNot(HaveOccurred())
-      Expect(version).ShouldNot(BeEmpty())
-      versionNum, parseErr := strconv.Atoi(version)
-      Expect(parseErr).ShouldNot(HaveOccurred())
-      Expect(versionNum).Should(BeNumerically(">=", 130008))
-    })
-
-    It("should return version string that can be parsed as integer", func() {
-      version, err := utils.EnsureDBVersion(writePool)
-      Expect(err).ShouldNot(HaveOccurred())
-      Expect(version).ShouldNot(BeEmpty())
-      versionNum, parseErr := strconv.Atoi(version)
-      Expect(parseErr).ShouldNot(HaveOccurred())
-      Expect(versionNum).Should(BeNumerically(">", 0))
-    })
-
-    It("should return version that meets minimum requirements", func() {
-      version, err := utils.EnsureDBVersion(writePool)
-      Expect(err).ShouldNot(HaveOccurred())
-      Expect(version).ShouldNot(BeEmpty())
-      versionNum, parseErr := strconv.Atoi(version)
-      Expect(parseErr).ShouldNot(HaveOccurred())
-      Expect(versionNum).Should(BeNumerically(">=", 130008))
-    })
-
-    It("should handle database connection properly", func() {
-      version, err := utils.EnsureDBVersion(writePool)
-      Expect(err).ShouldNot(HaveOccurred())
-      Expect(version).ShouldNot(BeEmpty())
-      Expect(version).Should(BeAssignableToTypeOf(""))
-    })
-
-    It("should return consistent results on multiple calls", func() {
-      version1, err1 := utils.EnsureDBVersion(writePool)
-      version2, err2 := utils.EnsureDBVersion(writePool)
-      Expect(err1).ShouldNot(HaveOccurred())
-      Expect(err2).ShouldNot(HaveOccurred())
-      Expect(version1).Should(Equal(version2))
-    })
+    It("returns a numeric server_version_num meeting minimum and is idempotent", func() {
+      v1, err := utils.EnsureDBVersion(writePool)
+      Expect(err).ShouldNot(HaveOccurred())
+      Expect(v1).ShouldNot(BeEmpty())
+      n, perr := strconv.Atoi(v1)
+      Expect(perr).ShouldNot(HaveOccurred())
+      Expect(n).Should(BeNumerically(">=", 130008)) // 13.8 minimum
+
+      v2, err2 := utils.EnsureDBVersion(writePool)
+      Expect(err2).ShouldNot(HaveOccurred())
+      Expect(v2).Should(Equal(v1))
+    })

Also applies to: 73-80, 82-90


131-156: Reuse the suite’s writePool instead of creating another container; also tighten format assertions.

Avoids an extra container spin and keeps the spec fast. Assert 6-digit length explicitly.

-      version := os.Getenv("POSTGRES_VERSION")
-      if version == "" {
-        version = "14"
-      }
-      database := instance.PostgresDB(version)
-      db := database.(*PQDatabase.Postgres)
-      writePool := db.WritePool
-      versionStr, err := utils.EnsureDBVersion(writePool)
-      Expect(err).ShouldNot(HaveOccurred())
-      versionNum, parseErr := strconv.Atoi(versionStr)
-      Expect(parseErr).ShouldNot(HaveOccurred())
-      Expect(versionNum).Should(BeNumerically(">=", 100000))
-      Expect(versionNum).Should(BeNumerically("<=", 999999))
-      err = db.Close()
-      Expect(err).ShouldNot(HaveOccurred())
+      versionStr, err := utils.EnsureDBVersion(writePool)
+      Expect(err).ShouldNot(HaveOccurred())
+      versionNum, parseErr := strconv.Atoi(versionStr)
+      Expect(parseErr).ShouldNot(HaveOccurred())
+      // 6 digits (MMmmpp) for server_version_num >= 10.0.0
+      Expect(len(versionStr)).To(Equal(6))
+      Expect(versionNum).Should(BeNumerically(">=", 100000))
+      Expect(versionNum).Should(BeNumerically("<=", 999999))
internal/info.go (1)

26-26: Version sync verified
Swagger JSON docs and proto openapi version both match “v1.4.4”. The only “v1.4.3” hits are indirect dependencies (github.com/go-logr/logr) in go.mod/go.sum, not the service version.
Optional: switch to build-time injection of Version via ldflags to prevent future drift.

internal/schema/linkedSchema_test.go (1)

1448-1449: Consider documenting why these tests are skipped.

While the skip messages explain the difficulty, it would be helpful to add TODO comments indicating whether these edge cases should be tested through other means (e.g., mocking, test helpers).

-			Skip("Skipping test for undefined child type - difficult to trigger with normal schema compilation")
+			// TODO: Consider adding test helpers to create malformed schema structures for these edge cases
+			Skip("Skipping test for undefined child type - difficult to trigger with normal schema compilation")

Also applies to: 1456-1457, 1464-1465

internal/storage/postgres/schemaReader_test.go (1)

255-259: Consider extracting common error assertion logic.

The error assertion pattern is repeated many times. Consider creating a helper function to reduce duplication.

Add a helper function at the beginning of the test file:

func expectDatabaseError(err error) {
	Expect(err).Should(HaveOccurred())
	Expect(err.Error()).Should(Or(
		Equal(base.ErrorCode_ERROR_CODE_SQL_BUILDER.String()),
		Equal(base.ErrorCode_ERROR_CODE_EXECUTION.String()),
		Equal(base.ErrorCode_ERROR_CODE_SCAN.String()),
	))
}

Then simplify the assertions:

-				Expect(err).Should(HaveOccurred())
-				Expect(err.Error()).Should(Or(
-					Equal(base.ErrorCode_ERROR_CODE_SQL_BUILDER.String()),
-					Equal(base.ErrorCode_ERROR_CODE_EXECUTION.String()),
-					Equal(base.ErrorCode_ERROR_CODE_SCAN.String()),
-				))
+				expectDatabaseError(err)

Also applies to: 274-278, 293-297, 312-317, 334-338, 353-357, 372-376

internal/storage/postgres/tenantWriter_test.go (1)

99-116: Duplicate test descriptions could be more specific.

Several tests have similar descriptions but test different error paths. Consider making the test names more specific to indicate which exact error path is being tested.

-			It("should handle transaction begin error", func() {
+			It("should handle transaction begin error when starting delete operation", func() {
-			It("should handle batch execution error", func() {
+			It("should handle batch execution error during delete operations", func() {
-			It("should handle commit error after batch execution", func() {
+			It("should handle transaction commit error after successful batch execution", func() {

Also applies to: 118-135, 137-154, 156-173, 175-192, 194-211

internal/storage/postgres/dataWriter_test.go (1)

545-548: Consider creating a test helper for common error assertions.

Similar to the suggestion for schemaReader_test.go, the datastore error assertions are repeated frequently.

Add a helper function:

func expectDatastoreOrRetryError(err error) {
	Expect(err).Should(HaveOccurred())
	Expect(err.Error()).Should(Or(
		Equal(base.ErrorCode_ERROR_CODE_DATASTORE.String()),
		Equal(base.ErrorCode_ERROR_CODE_ERROR_MAX_RETRIES.String()),
	))
}

Then use it to simplify assertions:

-				Expect(err).Should(HaveOccurred())
-				// The error could be various types depending on when the connection fails
-				Expect(err.Error()).Should(Or(
-					Equal(base.ErrorCode_ERROR_CODE_DATASTORE.String()),
-					Equal(base.ErrorCode_ERROR_CODE_ERROR_MAX_RETRIES.String()),
-				))
+				expectDatastoreOrRetryError(err)

Also applies to: 569-572, 593-596, 617-620, 641-644

internal/storage/postgres/gc/gc_test.go (2)

361-366: Prefer errors.Is for context cancellation assertion

Use errors.Is to be resilient to wrapped errors from Start.

-			// Start the garbage collector with the cancelled context
-			err := garbageCollector.Start(ctx)
-
-			Expect(err).Should(HaveOccurred())
-			Expect(err).Should(Equal(context.Canceled))
+			// Start the garbage collector with the cancelled context
+			err := garbageCollector.Start(ctx)
+			Expect(errors.Is(err, context.Canceled)).To(BeTrue())

Add the import:

import "errors"

368-587: DRY the “closed DB” setup and honor POSTGRES_VERSION

Each test re-creates and closes a brand-new Postgres container with a hard-coded "14", which is slow and ignores POSTGRES_VERSION. Factor a small helper that reads the env and returns a closed DB; then reuse it across the specs.

 Context("Error Handling", func() {
+    // Helper to provision a closed DB instance respecting POSTGRES_VERSION
+    getClosedDB := func() *PQDatabase.Postgres {
+        version := os.Getenv("POSTGRES_VERSION")
+        if version == "" {
+            version = "14"
+        }
+        d := instance.PostgresDB(version).(*PQDatabase.Postgres)
+        _ = d.Close()
+        return d
+    }
@@
-            // Create a garbage collector with a closed database connection
-            closedDB := instance.PostgresDB("14")
-            err := closedDB.Close()
-            Expect(err).ShouldNot(HaveOccurred())
-
-            badGC := NewGC(
-                closedDB.(*PQDatabase.Postgres),
+            badGC := NewGC(
+                getClosedDB(),
                 Window(5*time.Second),
             )

Apply the same pattern to the other tests in this context to remove duplication and container churn.

internal/storage/postgres/gc/options_test.go (1)

16-26: Avoid spinning a real Postgres for option setters

These tests only assert in-memory fields; using a real container slows CI and can flake. Use a lightweight stub Postgres instead and skip Close unless pools are non-nil.

-	BeforeEach(func() {
-		version := "14"
-		db = instance.PostgresDB(version).(*PQDatabase.Postgres)
-	})
+	BeforeEach(func() {
+		// No DB I/O needed for options; a zero-value stub is enough.
+		db = &PQDatabase.Postgres{}
+	})
@@
-	AfterEach(func() {
-		if db != nil {
-			err := db.Close()
-			Expect(err).ShouldNot(HaveOccurred())
-		}
-	})
+	AfterEach(func() {
+		// Close only if pools exist (when running locally against a real DB).
+		if db != nil && (db.ReadPool != nil || db.WritePool != nil) {
+			err := db.Close()
+			Expect(err).ShouldNot(HaveOccurred())
+		}
+	})
internal/storage/postgres/bundleReader_test.go (3)

140-149: Apply the same dedicated-closed-DB pattern here

Mirror the approach used above to avoid closing the shared db.

-			closedDB := db.(*PQDatabase.Postgres)
-			err := closedDB.Close()
-			Expect(err).ShouldNot(HaveOccurred())
+			version := os.Getenv("POSTGRES_VERSION")
+			if version == "" {
+				version = "14"
+			}
+			closedDB := instance.PostgresDB(version).(*PQDatabase.Postgres)
+			_ = closedDB.Close()

194-201: Repeat: isolate error injection from suite DB

Use a fresh closed instance rather than closing the shared one.

-			closedDB := db.(*PQDatabase.Postgres)
-			err := closedDB.Close()
-			Expect(err).ShouldNot(HaveOccurred())
+			version := os.Getenv("POSTGRES_VERSION")
+			if version == "" {
+				version = "14"
+			}
+			closedDB := instance.PostgresDB(version).(*PQDatabase.Postgres)
+			_ = closedDB.Close()

120-129: Use a dedicated closed DB instance for error injection
Closing the shared suite DB (db) can disrupt other specs. Instead, instantiate a fresh Postgres instance via instance.PostgresDB(version), close that, and pass it to NewBundleReader. Postgres.Close() unconditionally closes both pools and returns nil (idempotent).

-   // trigger SQL builder error on shared DB
-   closedDB := db.(*PQDatabase.Postgres)
-   err := closedDB.Close()
-   Expect(err).ShouldNot(HaveOccurred())
+   // trigger SQL builder error on a dedicated closed DB
+   version := os.Getenv("POSTGRES_VERSION")
+   if version == "" {
+       version = "14"
+   }
+   closedDB := instance.PostgresDB(version).(*PQDatabase.Postgres)
+   _ = closedDB.Close()

Ensure you import

"github.com/Permify/permify/internal/storage/postgres/instance"
internal/storage/postgres/dataReader_test.go (3)

496-506: Extract a helper for a closed DB and avoid closing the suite DB

Multiple specs close the shared db, which risks interfering with AfterEach or other specs. Add a small helper returning a dedicated closed DB (respecting POSTGRES_VERSION) and use it across the Error Handling context.

-	Context("Error Handling", func() {
+	Context("Error Handling", func() {
+		getClosedDB := func() *PQDatabase.Postgres {
+			version := os.Getenv("POSTGRES_VERSION")
+			if version == "" {
+				version = "14"
+			}
+			d := instance.PostgresDB(version).(*PQDatabase.Postgres)
+			_ = d.Close()
+			return d
+		}

501-507: Use the helper instead of closing the shared db

Replace ad-hoc close with getClosedDB() to isolate failure injection.

-				// Create a dataReader with a closed database to trigger errors
-				closedDB := db.(*PQDatabase.Postgres)
-				err := closedDB.Close()
-				Expect(err).ShouldNot(HaveOccurred())
-
-				readerWithClosedDB := NewDataReader(closedDB)
+				// Create a dataReader with a dedicated closed database
+				readerWithClosedDB := NewDataReader(getClosedDB())

Apply the same change to the other specs in this Error Handling context that close db directly.


982-1000: HeadSnapshot SQL/scan error tests: also use the helper

Keep the suite db open; inject failures via getClosedDB().

-				// Create a dataReader with a closed database to trigger SQL builder error
-				closedDB := db.(*PQDatabase.Postgres)
-				err := closedDB.Close()
-				Expect(err).ShouldNot(HaveOccurred())
-
-				readerWithClosedDB := NewDataReader(closedDB)
+				// Create a dataReader with a dedicated closed database
+				readerWithClosedDB := NewDataReader(getClosedDB())
internal/schema/walker_test.go (2)

206-212: Make error-code assertions resilient to wrapping

Direct string equality on err.Error() is brittle if implementations wrap errors. Prefer matching by substring or a small helper to assert codes.

Apply this pattern (example for Lines 210, 235, 309, 341, 437, 469):

-Expect(err.Error()).Should(Equal(base.ErrorCode_ERROR_CODE_ENTITY_DEFINITION_NOT_FOUND.String()))
+Expect(err).To(HaveOccurred())
+Expect(err.Error()).To(ContainSubstring(base.ErrorCode_ERROR_CODE_ENTITY_DEFINITION_NOT_FOUND.String()))

Optionally add a tiny helper at file bottom and replace all call sites:

func expectErrCode(err error, code base.ErrorCode) {
    Expect(err).To(HaveOccurred())
    Expect(err.Error()).To(ContainSubstring(code.String()))
}

Also applies to: 233-236, 308-310, 340-342, 436-438, 468-470


257-262: Compare sentinel errors with errors.Is

If ErrUnimplemented might be wrapped later, equality checks can start failing. Use errors.Is to future-proof.

Example:

-Expect(err).Should(Equal(ErrUnimplemented))
+Expect(errors.Is(err, ErrUnimplemented)).To(BeTrue())

Note: add errors import.

Also applies to: 361-368, 393-398

internal/authn/oidc/authn_test.go (1)

664-677: Skip rationale is fine; add link to code path for traceability (nit)

Optional: reference the exact function/lines in authn.go in the Skip message to aid future maintainers.

internal/storage/postgres/watch_test.go (1)

232-246: Error-message coupling is fragile; keep Substring but consider categorizing by phase

These getChanges error-path assertions depend on specific pgx messages ("encode text status undefined status"). If pgx changes text, tests will be noisy. Consider asserting higher-level phase (builder/exec/scan/unmarshal) via sentinel wrapping in production code, or at least grouping assertions with Or() for variations.

Also applies to: 248-262, 264-276, 278-291, 293-306, 308-321, 323-336

internal/storage/postgres/utils/common_test.go (6)

57-67: End spans to avoid leaks in longer runs

Add AfterEach to end the span created in BeforeEach.

Apply:

 		BeforeEach(func() {
 			ctx = context.Background()
 			_, span = noop.NewTracerProvider().Tracer("test").Start(ctx, "test-span")
 		})
+		AfterEach(func() {
+			span.End()
+		})

174-186: Tighten bounds to the known backoff window (reduce flakiness)

Base backoff for retries=0 is 20ms with jitter up to +10ms. Use bounds that reflect this.

Apply:

-			// Should have waited at least some time (backoff + jitter)
-			Expect(elapsed).Should(BeNumerically(">", 10*time.Millisecond))
-			// But not too long (should be less than our timeout)
-			Expect(elapsed).Should(BeNumerically("<", 90*time.Millisecond))
+			// 20ms <= elapsed < 100ms (with room for scheduler)
+			Expect(elapsed).Should(BeNumerically(">=", 18*time.Millisecond))
+			Expect(elapsed).Should(BeNumerically("<", 100*time.Millisecond))

188-198: Cancellation threshold is tight; relax to avoid CI jitter

Immediate select on ctx.Done() is near-instant but 10ms can be flaky on shared runners.

Apply:

-			Expect(elapsed).Should(BeNumerically("<", 10*time.Millisecond))
+			Expect(elapsed).Should(BeNumerically("<=", 30*time.Millisecond))

200-216: Strengthen assertion: higher retries must yield materially longer backoff

Given retries=2 ⇒ base ≥80ms (before jitter), assert a meaningful increase.

Apply:

-			// Higher retry count should generally result in longer backoff
-			// (though jitter might make this not always true, so we use a reasonable threshold)
-			Expect(elapsed2).Should(BeNumerically(">=", time.Duration(float64(elapsed1)*0.5)))
+			// Higher retry count should yield a noticeably longer wait
+			Expect(elapsed2).Should(BeNumerically(">", elapsed1))
+			Expect(elapsed2).Should(BeNumerically(">=", 60*time.Millisecond))

218-241: “Secure random” claim isn’t verifiable here; assert jittered range and uniqueness instead

Current equality check can still pass with identical durations by coincidence. Increase samples and assert range + uniqueness.

Apply:

-			durations := make([]time.Duration, 5)
-			for i := 0; i < 5; i++ {
+			durations := make([]time.Duration, 10)
+			for i := 0; i < 10; i++ {
 				start := time.Now()
 				utils.WaitWithBackoff(ctx, "test-tenant", 0)
 				durations[i] = time.Since(start)
 			}
 
-			// Should have some variation in durations due to jitter
-			hasVariation := false
-			for i := 1; i < len(durations); i++ {
-				if durations[i] != durations[0] {
-					hasVariation = true
-					break
-				}
-			}
-			Expect(hasVariation).Should(BeTrue())
+			// Each duration should be within the expected jittered window
+			for _, d := range durations {
+				Expect(d).Should(BeNumerically(">=", 18*time.Millisecond))
+				Expect(d).Should(BeNumerically("<", 100*time.Millisecond))
+			}
+			// And at least one should differ
+			unique := map[time.Duration]struct{}{}
+			for _, d := range durations {
+				unique[d] = struct{}{}
+			}
+			Expect(len(unique)).Should(BeNumerically(">", 1))

Also consider renaming the test to “should introduce jitter (non-deterministic backoff)”.


285-314: LGTM: Span integration smoke tests

Good sanity check that spans are used without panics. If needed later, introduce a tiny test span implementation to assert RecordError/SetStatus calls.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between db773e8 and d8de596.

⛔ Files ignored due to path filters (1)
  • pkg/pb/base/v1/openapi.pb.go is excluded by !**/*.pb.go
📒 Files selected for processing (25)
  • docs/api-reference/apidocs.swagger.json (1 hunks)
  • docs/api-reference/openapiv2/apidocs.swagger.json (1 hunks)
  • internal/authn/oidc/adapter_test.go (1 hunks)
  • internal/authn/oidc/authn_test.go (3 hunks)
  • internal/authn/preshared/authn_test.go (1 hunks)
  • internal/info.go (1 hunks)
  • internal/schema/linkedSchema_test.go (1 hunks)
  • internal/schema/walker_test.go (2 hunks)
  • internal/storage/postgres/bundleReader_test.go (1 hunks)
  • internal/storage/postgres/consts.go (0 hunks)
  • internal/storage/postgres/dataReader_test.go (1 hunks)
  • internal/storage/postgres/dataWriter_test.go (2 hunks)
  • internal/storage/postgres/gc/gc.go (0 hunks)
  • internal/storage/postgres/gc/gc_test.go (1 hunks)
  • internal/storage/postgres/gc/options_test.go (1 hunks)
  • internal/storage/postgres/postgres_test.go (2 hunks)
  • internal/storage/postgres/schemaReader_test.go (1 hunks)
  • internal/storage/postgres/tenantWriter_test.go (1 hunks)
  • internal/storage/postgres/utils/common.go (0 hunks)
  • internal/storage/postgres/utils/common_test.go (2 hunks)
  • internal/storage/postgres/utils/filter_test.go (1 hunks)
  • internal/storage/postgres/utils/version_test.go (1 hunks)
  • internal/storage/postgres/watch.go (0 hunks)
  • internal/storage/postgres/watch_test.go (2 hunks)
  • proto/base/v1/openapi.proto (1 hunks)
💤 Files with no reviewable changes (4)
  • internal/storage/postgres/watch.go
  • internal/storage/postgres/gc/gc.go
  • internal/storage/postgres/utils/common.go
  • internal/storage/postgres/consts.go
🧰 Additional context used
🧬 Code graph analysis (14)
internal/schema/walker_test.go (5)
pkg/dsl/parser/parser.go (1)
  • NewParser (59-78)
pkg/dsl/compiler/compiler.go (1)
  • NewCompiler (25-30)
internal/schema/walker.go (1)
  • NewWalker (19-24)
internal/schema/schema.go (1)
  • NewSchemaFromEntityAndRuleDefinitions (30-73)
pkg/pb/base/v1/errors.pb.go (3)
  • ErrorCode_ERROR_CODE_ENTITY_DEFINITION_NOT_FOUND (70-70)
  • ErrorCode_ERROR_CODE_UNDEFINED_CHILD_KIND (38-38)
  • ErrorCode_ERROR_CODE_UNDEFINED_RELATION_REFERENCE (39-39)
internal/authn/preshared/authn_test.go (2)
internal/config/config.go (1)
  • Preshared (67-69)
internal/authn/preshared/authn.go (1)
  • NewKeyAuthn (22-33)
internal/storage/postgres/gc/options_test.go (3)
pkg/database/postgres/postgres.go (1)
  • Postgres (22-35)
internal/storage/postgres/instance/instance.go (1)
  • PostgresDB (20-89)
internal/storage/postgres/gc/gc.go (1)
  • NewGC (31-45)
internal/storage/postgres/utils/common_test.go (2)
internal/storage/postgres/utils/common.go (4)
  • HandleError (101-124)
  • IsContextRelatedError (127-137)
  • IsSerializationRelatedError (140-146)
  • WaitWithBackoff (150-168)
pkg/pb/base/v1/errors.pb.go (3)
  • ErrorCode_ERROR_CODE_INTERNAL (82-82)
  • ErrorCode_ERROR_CODE_CANCELLED (83-83)
  • ErrorCode_ERROR_CODE_SERIALIZATION (100-100)
internal/storage/postgres/bundleReader_test.go (3)
pkg/database/postgres/postgres.go (1)
  • Postgres (22-35)
internal/storage/postgres/bundleReader.go (1)
  • NewBundleReader (25-30)
pkg/pb/base/v1/errors.pb.go (4)
  • ErrorCode_ERROR_CODE_SQL_BUILDER (84-84)
  • ErrorCode_ERROR_CODE_SCAN (87-87)
  • ErrorCode_ERROR_CODE_INVALID_ARGUMENT (58-58)
  • ErrorCode_ERROR_CODE_CANCELLED (83-83)
internal/storage/postgres/utils/version_test.go (2)
pkg/database/postgres/postgres.go (1)
  • Postgres (22-35)
internal/storage/postgres/instance/instance.go (1)
  • PostgresDB (20-89)
internal/schema/linkedSchema_test.go (3)
pkg/pb/base/v1/base.pb.go (3)
  • Entrance (1200-1209)
  • Entrance (1224-1224)
  • Entrance (1239-1241)
internal/schema/linkedSchema.go (1)
  • NewLinkedGraph (30-34)
internal/schema/schema.go (1)
  • NewSchemaFromEntityAndRuleDefinitions (30-73)
internal/storage/postgres/tenantWriter_test.go (2)
pkg/database/postgres/postgres.go (1)
  • Postgres (22-35)
pkg/pb/base/v1/errors.pb.go (3)
  • ErrorCode_ERROR_CODE_EXECUTION (86-86)
  • ErrorCode_ERROR_CODE_SQL_BUILDER (84-84)
  • ErrorCode_ERROR_CODE_SCAN (87-87)
internal/storage/postgres/schemaReader_test.go (4)
pkg/database/postgres/postgres.go (1)
  • Postgres (22-35)
internal/storage/postgres/schemaReader.go (1)
  • NewSchemaReader (30-35)
pkg/pb/base/v1/errors.pb.go (6)
  • ErrorCode_ERROR_CODE_SQL_BUILDER (84-84)
  • ErrorCode_ERROR_CODE_EXECUTION (86-86)
  • ErrorCode_ERROR_CODE_SCAN (87-87)
  • ErrorCode_ERROR_CODE_INTERNAL (82-82)
  • ErrorCode_ERROR_CODE_SCHEMA_NOT_FOUND (68-68)
  • ErrorCode_ERROR_CODE_INVALID_CONTINUOUS_TOKEN (54-54)
pkg/database/pagination.go (3)
  • NewPagination (27-36)
  • Size (7-11)
  • Token (14-18)
internal/storage/postgres/gc/gc_test.go (3)
internal/storage/postgres/instance/instance.go (1)
  • PostgresDB (20-89)
internal/storage/postgres/gc/gc.go (1)
  • NewGC (31-45)
pkg/database/postgres/postgres.go (1)
  • Postgres (22-35)
internal/storage/postgres/dataWriter_test.go (3)
pkg/pb/base/v1/errors.pb.go (3)
  • ErrorCode_ERROR_CODE_MAX_DATA_PER_WRITE_EXCEEDED (63-63)
  • ErrorCode_ERROR_CODE_DATASTORE (94-94)
  • ErrorCode_ERROR_CODE_ERROR_MAX_RETRIES (90-90)
pkg/database/postgres/postgres.go (1)
  • Postgres (22-35)
internal/storage/postgres/dataWriter.go (1)
  • NewDataWriter (34-39)
internal/authn/oidc/authn_test.go (2)
internal/authn/oidc/authn.go (1)
  • NewOidcAuthn (53-111)
internal/config/config.go (1)
  • Oidc (72-80)
internal/storage/postgres/dataReader_test.go (5)
pkg/pb/base/v1/base.pb.go (9)
  • TupleFilter (2038-2046)
  • TupleFilter (2061-2061)
  • TupleFilter (2076-2078)
  • AttributeFilter (1982-1989)
  • AttributeFilter (2004-2004)
  • AttributeFilter (2019-2021)
  • RelationReference (1143-1152)
  • RelationReference (1167-1167)
  • RelationReference (1182-1184)
pkg/database/postgres/postgres.go (1)
  • Postgres (22-35)
internal/storage/postgres/dataReader.go (1)
  • NewDataReader (35-40)
pkg/database/pagination.go (6)
  • CursorPagination (52-56)
  • NewCursorPagination (59-68)
  • Cursor (71-75)
  • Sort (78-82)
  • Pagination (21-24)
  • NewPagination (27-36)
pkg/pb/base/v1/errors.pb.go (5)
  • ErrorCode_ERROR_CODE_INTERNAL (82-82)
  • ErrorCode_ERROR_CODE_INVALID_CONTINUOUS_TOKEN (54-54)
  • ErrorCode_ERROR_CODE_SQL_BUILDER (84-84)
  • ErrorCode_ERROR_CODE_EXECUTION (86-86)
  • ErrorCode_ERROR_CODE_SCAN (87-87)
internal/storage/postgres/watch_test.go (2)
internal/storage/postgres/watch.go (2)
  • Watch (26-35)
  • NewWatcher (38-43)
pkg/database/postgres/postgres.go (1)
  • Postgres (22-35)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (3)
  • GitHub Check: Analyze (go)
  • GitHub Check: Test with Coverage
  • GitHub Check: Test with Coverage
🔇 Additional comments (26)
internal/authn/preshared/authn_test.go (1)

37-55: LGTM: solid coverage for empty and nil preshared keys.

Tests accurately reflect NewKeyAuthn’s contract for invalid input.

internal/storage/postgres/utils/filter_test.go (1)

238-490: Nice additions: strong coverage for single-ID/attribute cases.

The single-value paths (switching from IN to =) are well captured and improve confidence in builders.

internal/storage/postgres/utils/version_test.go (2)

1-2: Good choice to use an external test package.

Testing via utils_test enforces validation of public behavior only. LGTM.


92-121: Gate cross-version container tests behind POSTGRES_VERSIONS
Require POSTGRES_VERSIONS (e.g., “13,14,15,16”) and skip tests otherwise to avoid brittle, heavy specs.

-    It("should handle different PostgreSQL versions", func() {
-      versions := []string{"13", "14", "15", "16"}
-      for _, pgVersion := range versions {
-        if os.Getenv("POSTGRES_VERSION") != "" && os.Getenv("POSTGRES_VERSION") != pgVersion {
-          continue
-        }
+    It("handles multiple PostgreSQL versions when explicitly requested", func() {
+      csv := os.Getenv("POSTGRES_VERSIONS")
+      if csv == "" {
+        Skip("set POSTGRES_VERSIONS to run cross-version container tests")
+      }
+      for _, pgVersion := range strings.Split(csv, ",") {
         database := instance.PostgresDB(pgVersion)
         testDB := database.(*PQDatabase.Postgres)
         testPool := testDB.WritePool
         version, err := utils.EnsureDBVersion(testPool)
         Expect(err).ShouldNot(HaveOccurred())
         Expect(version).ShouldNot(BeEmpty())
         versionNum, parseErr := strconv.Atoi(version)
         Expect(parseErr).ShouldNot(HaveOccurred())
         Expect(versionNum).Should(BeNumerically(">=", 130008))
         err = testDB.Close()
         Expect(err).ShouldNot(HaveOccurred())
       }
     })

Ensure CI workflows skip these specs unless POSTGRES_VERSIONS is set or the integration label is applied.

docs/api-reference/openapiv2/apidocs.swagger.json (1)

6-6: Version metadata bumped to v1.4.4 — LGTM.

internal/info.go (1)

26-26: Update Version to v1.4.4 — looks good.

docs/api-reference/apidocs.swagger.json (1)

6-6: Version field set to v1.4.4 — LGTM.

proto/base/v1/openapi.proto (1)

12-12: OpenAPI info.version bumped to v1.4.4 — LGTM.

internal/schema/linkedSchema_test.go (1)

1254-1615: LGTM! Comprehensive error handling test coverage added.

The new error handling test context covers all major error scenarios for LinkedEntrances:

  • Permission/attribute not found
  • Entity/relation definition not found
  • Error propagation in recursive calls

The use of schema manipulation (deleting definitions) to trigger errors is a good testing approach.

internal/storage/postgres/schemaReader_test.go (1)

241-660: LGTM! Well-structured error handling tests for SchemaReader.

The error handling test context follows a consistent pattern:

  1. Creating a closed DB connection to trigger errors
  2. Testing different error paths (SQL builder, execution, scan errors)
  3. Using Or matcher for multiple valid error codes
  4. Testing schema-not-found scenarios with appropriate error codes

This approach provides good coverage for error paths.

internal/storage/postgres/tenantWriter_test.go (1)

76-213: LGTM! Comprehensive error handling for TenantWriter operations.

Good coverage of various error scenarios in DeleteTenant including:

  • Transaction begin/commit errors
  • Batch execution/close errors
  • Query row errors

The systematic approach to testing each failure point is excellent.

internal/storage/postgres/dataWriter_test.go (3)

5-5: LGTM! Required import for dynamic test data generation.

The fmt package import is necessary for generating test data in the error handling tests.


506-1116: Excellent comprehensive error handling test coverage!

The error handling tests are well-organized with nested contexts for:

  • Write operations (max data exceeded, serialization errors, retry logic)
  • Delete operations (serialization errors, max retries)
  • RunBundle operations (transaction errors)
  • Internal error handling for each method

The tests effectively simulate various failure scenarios using closed DB connections.


508-525: Good test for max data per write validation.

The test properly validates that attempting to write more than 1000 items returns the appropriate error code.

internal/authn/oidc/adapter_test.go (1)

1-153: Solid coverage and clear assertions

The suite is readable and validates levels, messages, and kv-pairs well. The odd kv-pair case is a nice touch.

internal/schema/walker_test.go (1)

9-10: Import alias looks good

Alias for base error codes is appropriate and keeps assertions readable.

internal/authn/oidc/authn_test.go (4)

5-8: New imports are appropriate

encoding/json and net/http are correctly added for custom OIDC config and HTTP error-path tests.


524-602: Solid backoff parameter validation tests

Good coverage for zero/negative values across interval, max retries, and frequency.


736-820: OIDC configuration error-path tests look great

Issuer/JWKS validation and invalid JSON scenarios are well-covered.


822-858: HTTP status error-path test is correct

404 case surfaces as expected via constructor fetch.

internal/storage/postgres/watch_test.go (1)

13-13: Import of types.XID8 is correct

Matches getChanges signature and avoids ad-hoc definitions.

internal/storage/postgres/utils/common_test.go (5)

21-33: LGTM: SnapshotQuery test is precise and valuable

Exact SQL assertion is fine here given the function’s critical semantics.


68-109: LGTM: Context-related error handling cases cover the branches

Tests correctly exercise cancellation, deadline, and "conn closed".


111-132: LGTM: Serialization and operational branches validated

Good coverage for both serialization and default error paths.


134-171: LGTM: Error detection helpers have comprehensive positive/negative cases

Solid coverage and clear expectations.


36-55: No placeholder format mismatch — “?” is Squirrel’s default
These DeleteBuilders don’t call PlaceholderFormat, so they use Squirrel’s default question-mark placeholders. Tests correctly expect “?” and need no change.

Likely an incorrect or invalid review comment.

Comment on lines +679 to +735
Context("Context Cancellation During Retry", func() {
It("should handle context cancellation during retry", func() {
// create authenticator with short backoff frequency to trigger retries quickly
ctx, cancel := context.WithCancel(context.Background())
auth, err := NewOidcAuthn(ctx, config.Oidc{
Audience: audience,
Issuer: issuerURL,
RefreshInterval: 5 * time.Minute,
BackoffInterval: 12 * time.Second,
BackoffMaxRetries: 3,
BackoffFrequency: 2 * time.Second, // Longer frequency to allow cancellation
})
Expect(err).To(BeNil())

// Create a token with invalid key ID to trigger retries
now := time.Now()
claims := jwt.RegisteredClaims{
Issuer: issuerURL,
Subject: "user",
Audience: []string{audience},
ExpiresAt: &jwt.NumericDate{Time: now.AddDate(1, 0, 0)},
IssuedAt: &jwt.NumericDate{Time: now},
}

unsignedToken := createUnsignedToken(claims, jwt.SigningMethodRS256)
unsignedToken.Header["kid"] = "invalidkeyid" // This will trigger retries
idToken, err := fakeOidcProvider.SignIDToken(unsignedToken)
Expect(err).To(BeNil())

// Start authentication in a goroutine
authErr := make(chan error, 1)
go func() {
niceMd := make(metautils.NiceMD)
niceMd.Set("authorization", "Bearer "+idToken)
authErr <- auth.Authenticate(niceMd.ToIncoming(ctx))
}()

// Cancel the context after a short delay to interrupt the retry
time.Sleep(100 * time.Millisecond)
cancel()

// Wait for the authentication to complete and check for context cancellation error
select {
case err := <-authErr:
Expect(err).To(HaveOccurred())
// The error might be wrapped, so check for context cancellation in the error chain
Expect(err.Error()).To(Or(
ContainSubstring("context canceled"),
ContainSubstring("context cancelled"),
Equal(base.ErrorCode_ERROR_CODE_INVALID_BEARER_TOKEN.String()),
))
case <-time.After(5 * time.Second):
Fail("Authentication should have completed or been cancelled")
}
})
})

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Exercise true cancellation and relax assertion

This test cancels ctx, but assertions accept generic token errors too. Tighten it to actually validate cancellation signals.

Minimal tweak to assert cancellation (keeps current behavior if lib wraps):

-            select {
-            case err := <-authErr:
-                Expect(err).To(HaveOccurred())
-                // The error might be wrapped, so check for context cancellation in the error chain
-                Expect(err.Error()).To(Or(
-                    ContainSubstring("context canceled"),
-                    ContainSubstring("context cancelled"),
-                    Equal(base.ErrorCode_ERROR_CODE_INVALID_BEARER_TOKEN.String()),
-                ))
+            select {
+            case err := <-authErr:
+                Expect(err).To(HaveOccurred())
+                Expect(err.Error()).To(Or(
+                    ContainSubstring("context canceled"),
+                    ContainSubstring("context cancelled"),
+                ))
             case <-time.After(5 * time.Second):
                 Fail("Authentication should have completed or been cancelled")
             }

Consider shortening BackoffFrequency (e.g., 200ms) to reduce test wall time and flakiness.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
Context("Context Cancellation During Retry", func() {
It("should handle context cancellation during retry", func() {
// create authenticator with short backoff frequency to trigger retries quickly
ctx, cancel := context.WithCancel(context.Background())
auth, err := NewOidcAuthn(ctx, config.Oidc{
Audience: audience,
Issuer: issuerURL,
RefreshInterval: 5 * time.Minute,
BackoffInterval: 12 * time.Second,
BackoffMaxRetries: 3,
BackoffFrequency: 2 * time.Second, // Longer frequency to allow cancellation
})
Expect(err).To(BeNil())
// Create a token with invalid key ID to trigger retries
now := time.Now()
claims := jwt.RegisteredClaims{
Issuer: issuerURL,
Subject: "user",
Audience: []string{audience},
ExpiresAt: &jwt.NumericDate{Time: now.AddDate(1, 0, 0)},
IssuedAt: &jwt.NumericDate{Time: now},
}
unsignedToken := createUnsignedToken(claims, jwt.SigningMethodRS256)
unsignedToken.Header["kid"] = "invalidkeyid" // This will trigger retries
idToken, err := fakeOidcProvider.SignIDToken(unsignedToken)
Expect(err).To(BeNil())
// Start authentication in a goroutine
authErr := make(chan error, 1)
go func() {
niceMd := make(metautils.NiceMD)
niceMd.Set("authorization", "Bearer "+idToken)
authErr <- auth.Authenticate(niceMd.ToIncoming(ctx))
}()
// Cancel the context after a short delay to interrupt the retry
time.Sleep(100 * time.Millisecond)
cancel()
// Wait for the authentication to complete and check for context cancellation error
select {
case err := <-authErr:
Expect(err).To(HaveOccurred())
// The error might be wrapped, so check for context cancellation in the error chain
Expect(err.Error()).To(Or(
ContainSubstring("context canceled"),
ContainSubstring("context cancelled"),
Equal(base.ErrorCode_ERROR_CODE_INVALID_BEARER_TOKEN.String()),
))
case <-time.After(5 * time.Second):
Fail("Authentication should have completed or been cancelled")
}
})
})
// Wait for the authentication to complete and check for context cancellation error
select {
case err := <-authErr:
Expect(err).To(HaveOccurred())
// Ensure we actually got a cancellation, not just a token error
Expect(err.Error()).To(Or(
ContainSubstring("context canceled"),
ContainSubstring("context cancelled"),
))
case <-time.After(5 * time.Second):
Fail("Authentication should have completed or been cancelled")
}
🤖 Prompt for AI Agents
In internal/authn/oidc/authn_test.go around lines 679 to 735 the test cancels
the context but currently accepts generic token errors; change BackoffFrequency
to a much shorter value (e.g., 200*time.Millisecond) to speed retries and ensure
cancellation actually occurs, then tighten the assertion to require a
cancellation signal by checking errors.Is(err, context.Canceled) or scanning the
error chain for "context canceled"/"context cancelled" (and fail if only the
generic token error is present); keep the existing timeout guard but assert
cancellation explicitly so the test fails if only token errors are returned.

Comment on lines +245 to +262
It("should log context-related errors at debug level", func() {
var buf strings.Builder
logger := slog.New(slog.NewTextHandler(&buf, &slog.HandlerOptions{Level: slog.LevelDebug}))
ctx := context.WithValue(context.Background(), "logger", logger)

// Create a cancelled context
cancelCtx, cancel := context.WithCancel(context.Background())
cancel()

_, span := noop.NewTracerProvider().Tracer("test").Start(ctx, "test-span")
utils.HandleError(cancelCtx, span, errors.New("some error"), base.ErrorCode_ERROR_CODE_INTERNAL)

// Note: The actual logging happens in the HandleError function,
// but we can't easily capture it in this test setup.
// The test verifies the function doesn't panic and returns the expected error.
Expect(buf.String()).Should(ContainSubstring(""))
})

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Logging test currently asserts nothing; capture slog output and verify level/message

slog.*Context uses the default logger, not a logger in ctx. Set and restore the global default to capture logs and assert content.

Apply:

-			var buf strings.Builder
-			logger := slog.New(slog.NewTextHandler(&buf, &slog.HandlerOptions{Level: slog.LevelDebug}))
-			ctx := context.WithValue(context.Background(), "logger", logger)
+			var buf strings.Builder
+			prev := slog.Default()
+			defer slog.SetDefault(prev)
+			logger := slog.New(slog.NewTextHandler(&buf, &slog.HandlerOptions{Level: slog.LevelDebug}))
+			slog.SetDefault(logger)
 
 			// Create a cancelled context
 			cancelCtx, cancel := context.WithCancel(context.Background())
 			cancel()
 
-			_, span := noop.NewTracerProvider().Tracer("test").Start(ctx, "test-span")
+			_, span := noop.NewTracerProvider().Tracer("test").Start(context.Background(), "test-span")
 			utils.HandleError(cancelCtx, span, errors.New("some error"), base.ErrorCode_ERROR_CODE_INTERNAL)
 
-			// Note: The actual logging happens in the HandleError function,
-			// but we can't easily capture it in this test setup.
-			// The test verifies the function doesn't panic and returns the expected error.
-			Expect(buf.String()).Should(ContainSubstring(""))
+			Expect(buf.String()).Should(ContainSubstring("A context-related error occurred"))
+			Expect(buf.String()).Should(ContainSubstring("level=DEBUG"))
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
It("should log context-related errors at debug level", func() {
var buf strings.Builder
logger := slog.New(slog.NewTextHandler(&buf, &slog.HandlerOptions{Level: slog.LevelDebug}))
ctx := context.WithValue(context.Background(), "logger", logger)
// Create a cancelled context
cancelCtx, cancel := context.WithCancel(context.Background())
cancel()
_, span := noop.NewTracerProvider().Tracer("test").Start(ctx, "test-span")
utils.HandleError(cancelCtx, span, errors.New("some error"), base.ErrorCode_ERROR_CODE_INTERNAL)
// Note: The actual logging happens in the HandleError function,
// but we can't easily capture it in this test setup.
// The test verifies the function doesn't panic and returns the expected error.
Expect(buf.String()).Should(ContainSubstring(""))
})
It("should log context-related errors at debug level", func() {
var buf strings.Builder
prev := slog.Default()
defer slog.SetDefault(prev)
logger := slog.New(slog.NewTextHandler(&buf, &slog.HandlerOptions{Level: slog.LevelDebug}))
slog.SetDefault(logger)
// Create a cancelled context
cancelCtx, cancel := context.WithCancel(context.Background())
cancel()
_, span := noop.NewTracerProvider().Tracer("test").Start(context.Background(), "test-span")
utils.HandleError(cancelCtx, span, errors.New("some error"), base.ErrorCode_ERROR_CODE_INTERNAL)
Expect(buf.String()).Should(ContainSubstring("A context-related error occurred"))
Expect(buf.String()).Should(ContainSubstring("level=DEBUG"))
})

Comment on lines +263 to +271
It("should log serialization errors at debug level", func() {
ctx := context.Background()
_, span := noop.NewTracerProvider().Tracer("test").Start(ctx, "test-span")

err := utils.HandleError(ctx, span, errors.New("could not serialize access"), base.ErrorCode_ERROR_CODE_INTERNAL)

Expect(err).Should(HaveOccurred())
Expect(err.Error()).Should(Equal(base.ErrorCode_ERROR_CODE_SERIALIZATION.String()))
})
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Also assert serialization log path and level

Mirror the capture approach to validate debug-level serialization logging.

Apply:

-			ctx := context.Background()
-			_, span := noop.NewTracerProvider().Tracer("test").Start(ctx, "test-span")
-
-			err := utils.HandleError(ctx, span, errors.New("could not serialize access"), base.ErrorCode_ERROR_CODE_INTERNAL)
+			var buf strings.Builder
+			prev := slog.Default()
+			defer slog.SetDefault(prev)
+			logger := slog.New(slog.NewTextHandler(&buf, &slog.HandlerOptions{Level: slog.LevelDebug}))
+			slog.SetDefault(logger)
+			_, span := noop.NewTracerProvider().Tracer("test").Start(context.Background(), "test-span")
+
+			err := utils.HandleError(context.Background(), span, errors.New("could not serialize access"), base.ErrorCode_ERROR_CODE_INTERNAL)
 
 			Expect(err).Should(HaveOccurred())
 			Expect(err.Error()).Should(Equal(base.ErrorCode_ERROR_CODE_SERIALIZATION.String()))
+			Expect(buf.String()).Should(ContainSubstring("A serialization-related error occurred"))
+			Expect(buf.String()).Should(ContainSubstring("level=DEBUG"))

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
internal/storage/postgres/utils/common_test.go around lines 263 to 271: the test
verifies the returned error for serialization but does not assert that the
serialization path was logged at debug level; wrap the call in a log capture
(using the existing test logger harness or a test hook) before calling
utils.HandleError with the serialization error, then assert the captured log
contains the expected serialization message/path and that the log entry level is
debug (e.g., check message substring and level field), keeping existing error
assertions intact.

Comment on lines +273 to +283
It("should log operational errors at error level", func() {
ctx := context.Background()
_, span := noop.NewTracerProvider().Tracer("test").Start(ctx, "test-span")

operationalErr := errors.New("database connection failed")
err := utils.HandleError(ctx, span, operationalErr, base.ErrorCode_ERROR_CODE_INTERNAL)

Expect(err).Should(HaveOccurred())
Expect(err.Error()).Should(Equal(base.ErrorCode_ERROR_CODE_INTERNAL.String()))
})
})
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Assert operational errors are logged at error level

Capture slog and check both message and level.

Apply:

-			ctx := context.Background()
-			_, span := noop.NewTracerProvider().Tracer("test").Start(ctx, "test-span")
-
-			operationalErr := errors.New("database connection failed")
-			err := utils.HandleError(ctx, span, operationalErr, base.ErrorCode_ERROR_CODE_INTERNAL)
+			var buf strings.Builder
+			prev := slog.Default()
+			defer slog.SetDefault(prev)
+			logger := slog.New(slog.NewTextHandler(&buf, &slog.HandlerOptions{Level: slog.LevelDebug}))
+			slog.SetDefault(logger)
+			_, span := noop.NewTracerProvider().Tracer("test").Start(context.Background(), "test-span")
+
+			operationalErr := errors.New("database connection failed")
+			err := utils.HandleError(context.Background(), span, operationalErr, base.ErrorCode_ERROR_CODE_INTERNAL)
 
 			Expect(err).Should(HaveOccurred())
 			Expect(err.Error()).Should(Equal(base.ErrorCode_ERROR_CODE_INTERNAL.String()))
+			Expect(buf.String()).Should(ContainSubstring("An operational error occurred"))
+			Expect(buf.String()).Should(ContainSubstring("level=ERROR"))
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
It("should log operational errors at error level", func() {
ctx := context.Background()
_, span := noop.NewTracerProvider().Tracer("test").Start(ctx, "test-span")
operationalErr := errors.New("database connection failed")
err := utils.HandleError(ctx, span, operationalErr, base.ErrorCode_ERROR_CODE_INTERNAL)
Expect(err).Should(HaveOccurred())
Expect(err.Error()).Should(Equal(base.ErrorCode_ERROR_CODE_INTERNAL.String()))
})
})
It("should log operational errors at error level", func() {
// Set up a buffer to capture slog output, and restore the previous default logger afterwards
var buf strings.Builder
prev := slog.Default()
defer slog.SetDefault(prev)
logger := slog.New(slog.NewTextHandler(&buf, &slog.HandlerOptions{Level: slog.LevelDebug}))
slog.SetDefault(logger)
// Start the span and invoke HandleError
_, span := noop.NewTracerProvider().Tracer("test").Start(context.Background(), "test-span")
operationalErr := errors.New("database connection failed")
err := utils.HandleError(context.Background(), span, operationalErr, base.ErrorCode_ERROR_CODE_INTERNAL)
// Validate the returned error as before
Expect(err).Should(HaveOccurred())
Expect(err.Error()).Should(Equal(base.ErrorCode_ERROR_CODE_INTERNAL.String()))
// New assertions: ensure the log contains the expected message and error level
Expect(buf.String()).Should(ContainSubstring("An operational error occurred"))
Expect(buf.String()).Should(ContainSubstring("level=ERROR"))
})
🤖 Prompt for AI Agents
In internal/storage/postgres/utils/common_test.go around lines 273 to 283, the
test currently only asserts the returned error but does not capture or assert
structured slog output; modify the test to install a test slog handler (or
replace the global logger with a capturable handler) before calling
utils.HandleError, invoke the function with the operational error, then inspect
captured logs to assert there is a log entry whose message matches the
operational error context and whose level equals slog.LevelError (or "error");
finally restore the original logger and keep the existing error return
assertions.

Comment on lines +21 to +30
BeforeEach(func() {
version := os.Getenv("POSTGRES_VERSION")
if version == "" {
version = "14"
}

database := instance.PostgresDB(version)
db = database.(*PQDatabase.Postgres)
writePool = db.WritePool
})
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Avoid per-spec container setup; switch to BeforeAll/AfterAll and reuse one DB for the suite.

Spinning containers in every It is slow and flaky. Initialize once and reuse the pool across tests.

-    BeforeEach(func() {
+    BeforeAll(func() {
       version := os.Getenv("POSTGRES_VERSION")
       if version == "" {
         version = "14"
       }
 
       database := instance.PostgresDB(version)
       db = database.(*PQDatabase.Postgres)
       writePool = db.WritePool
     })
 
-    AfterEach(func() {
+    AfterAll(func() {
       err := db.Close()
       Expect(err).ShouldNot(HaveOccurred())
     })

Also applies to: 32-35

Comment on lines +124 to +130
Context("Version Constants", func() {
It("should have correct minimum PostgreSQL version constant", func() {
// Test that the constant is set to the expected value
// This tests the constant definition indirectly
Expect(130008).Should(Equal(130008)) // earliestPostgresVersion
})

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Remove the self-equality constant test — it provides no signal.

Expect(130008).To(Equal(130008)) cannot fail and doesn’t validate repo behavior. Either expose the minimum as an exported constant (or getter) and assert against it, or delete this spec.

-  It("should have correct minimum PostgreSQL version constant", func() {
-    // Test that the constant is set to the expected value
-    // This tests the constant definition indirectly
-    Expect(130008).Should(Equal(130008)) // earliestPostgresVersion
-  })
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
Context("Version Constants", func() {
It("should have correct minimum PostgreSQL version constant", func() {
// Test that the constant is set to the expected value
// This tests the constant definition indirectly
Expect(130008).Should(Equal(130008)) // earliestPostgresVersion
})
Context("Version Constants", func() {
// TODO: replace with a meaningful test that actually exercises
// the exported earliestPostgresVersion constant or its getter
})
🤖 Prompt for AI Agents
In internal/storage/postgres/utils/version_test.go around lines 124-130, the
test uses a self-equality assertion Expect(130008).To(Equal(130008)) which is
meaningless; either remove this spec or replace it to assert the actual
minimum-version symbol. Fix by referencing the package symbol (export the
constant or add a getter if needed) and change the test to
Expect(actualMinimumVersion).To(Equal(130008)), or simply delete the redundant
test block if exposing the constant is not desired.

Comment on lines +135 to +156
It("should handle get changes error", func() {
ctx := context.Background()

// Create a watcher with a closed database to trigger get changes error
closedDB := db.(*PQDatabase.Postgres)
err := closedDB.Close()
Expect(err).ShouldNot(HaveOccurred())

watcherWithClosedDB := NewWatcher(closedDB)

// Use a valid snapshot format but with closed database
_, errs := watcherWithClosedDB.Watch(ctx, "t1", "0")

// Wait for error
select {
case err := <-errs:
Expect(err).Should(HaveOccurred())
Expect(err.Error()).Should(ContainSubstring("illegal base64 data"))
case <-time.After(5 * time.Second):
Fail("Expected error but got timeout")
}

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Test doesn’t validate “get changes” path; it re-hits base64 decode

Using "0" snapshot triggers base64 decode error before any DB call, so the test title and assertion are misleading.

Write once to obtain a valid snapshot, then close DB and expect a pool/exec error:

-                // Create a watcher with a closed database to trigger get changes error
-                closedDB := db.(*PQDatabase.Postgres)
-                err := closedDB.Close()
-                Expect(err).ShouldNot(HaveOccurred())
-
-                watcherWithClosedDB := NewWatcher(closedDB)
-
-                // Use a valid snapshot format but with closed database
-                _, errs := watcherWithClosedDB.Watch(ctx, "t1", "0")
+                // Create a valid snapshot first
+                tup, terr := tuple.Tuple("organization:org-1#admin@user:user-1")
+                Expect(terr).ShouldNot(HaveOccurred())
+                tok, terr := dataWriter.Write(ctx, "t1",
+                    database.NewTupleCollection(tup),
+                    database.NewAttributeCollection(),
+                )
+                Expect(terr).ShouldNot(HaveOccurred())
+
+                // Now close DB to force downstream errors in getChanges
+                closedDB := db.(*PQDatabase.Postgres)
+                err := closedDB.Close()
+                Expect(err).ShouldNot(HaveOccurred())
+                watcherWithClosedDB := NewWatcher(closedDB)
+
+                _, errs := watcherWithClosedDB.Watch(ctx, "t1", tok.String())
 ...
-                case err := <-errs:
-                    Expect(err).Should(HaveOccurred())
-                    Expect(err.Error()).Should(ContainSubstring("illegal base64 data"))
+                case err := <-errs:
+                    Expect(err).Should(HaveOccurred())
+                    Expect(err.Error()).Should(ContainSubstring("closed pool"))

Comment on lines +160 to +182
It("should handle context cancellation", func() {
// Create a context that will be cancelled
ctx, cancel := context.WithCancel(context.Background())

_, errs := watcher.Watch(ctx, "t1", "0")

// Cancel the context after a short delay
go func() {
time.Sleep(100 * time.Millisecond)
cancel()
}()

// Wait for cancellation error
select {
case err := <-errs:
Expect(err).Should(HaveOccurred())
Expect(err.Error()).Should(ContainSubstring("illegal base64 data"))
case <-time.After(5 * time.Second):
Fail("Expected cancellation error but got timeout")
}

// Channels are closed by the Watch method internally
})
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Cancellation test isn’t asserting cancellation

Again, "0" snapshot triggers base64 error before cancellation takes effect. Provide a valid snapshot and assert cancel semantics.

Sketch:

-                _, errs := watcher.Watch(ctx, "t1", "0")
+                // Obtain a valid snapshot
+                tup, terr := tuple.Tuple("organization:org-1#admin@user:user-1")
+                Expect(terr).ShouldNot(HaveOccurred())
+                tok, terr := dataWriter.Write(ctx, "t1",
+                    database.NewTupleCollection(tup),
+                    database.NewAttributeCollection(),
+                )
+                Expect(terr).ShouldNot(HaveOccurred())
+                _, errs := watcher.Watch(ctx, "t1", tok.String())
 ...
-                case err := <-errs:
-                    Expect(err).Should(HaveOccurred())
-                    Expect(err.Error()).Should(ContainSubstring("illegal base64 data"))
+                case err := <-errs:
+                    Expect(err).Should(HaveOccurred())
+                    Expect(err.Error()).To(Or(
+                        ContainSubstring("context canceled"),
+                        ContainSubstring("context cancelled"),
+                        ContainSubstring("user request"),
+                    ))

Optionally replace the select with Eventually(errs).Should(Receive()) for cleaner timing.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
It("should handle context cancellation", func() {
// Create a context that will be cancelled
ctx, cancel := context.WithCancel(context.Background())
_, errs := watcher.Watch(ctx, "t1", "0")
// Cancel the context after a short delay
go func() {
time.Sleep(100 * time.Millisecond)
cancel()
}()
// Wait for cancellation error
select {
case err := <-errs:
Expect(err).Should(HaveOccurred())
Expect(err.Error()).Should(ContainSubstring("illegal base64 data"))
case <-time.After(5 * time.Second):
Fail("Expected cancellation error but got timeout")
}
// Channels are closed by the Watch method internally
})
It("should handle context cancellation", func() {
// Create a context that will be cancelled
ctx, cancel := context.WithCancel(context.Background())
// Obtain a valid snapshot
tup, terr := tuple.Tuple("organization:org-1#admin@user:user-1")
Expect(terr).ShouldNot(HaveOccurred())
tok, terr := dataWriter.Write(ctx, "t1",
database.NewTupleCollection(tup),
database.NewAttributeCollection(),
)
Expect(terr).ShouldNot(HaveOccurred())
_, errs := watcher.Watch(ctx, "t1", tok.String())
// Cancel the context after a short delay
go func() {
time.Sleep(100 * time.Millisecond)
cancel()
}()
// Wait for cancellation error
select {
case err := <-errs:
Expect(err).Should(HaveOccurred())
Expect(err.Error()).To(Or(
ContainSubstring("context canceled"),
ContainSubstring("context cancelled"),
ContainSubstring("user request"),
))
case <-time.After(5 * time.Second):
Fail("Expected cancellation error but got timeout")
}
// Channels are closed by the Watch method internally
})
🤖 Prompt for AI Agents
internal/storage/postgres/watch_test.go lines 160-182: the test uses snapshot
"0" which causes a base64 decode error before the context cancel takes effect;
replace the invalid snapshot with a valid snapshot value (e.g., a properly
base64-encoded snapshot or a known-good fixture used elsewhere in tests), cancel
the context as currently done, then assert that the received error is a
cancellation error (use errors.Is(err, context.Canceled) or check err.Error()
contains "context canceled") instead of asserting the base64 error; optionally
simplify timing by using Eventually(errs).Should(Receive()) to wait for the
error.

Comment on lines +186 to +200
It("should handle SQL builder error", func() {
ctx := context.Background()

// Create a watcher with a closed database to trigger SQL builder error
closedDB := db.(*PQDatabase.Postgres)
err := closedDB.Close()
Expect(err).ShouldNot(HaveOccurred())

watcherWithClosedDB := NewWatcher(closedDB)

_, err = watcherWithClosedDB.getRecentXIDs(ctx, 0, "t1")
Expect(err).Should(HaveOccurred())
Expect(err.Error()).Should(ContainSubstring("closed pool"))
})

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Verification agent

🧩 Analysis chain

Closing shared db inside tests may break AfterEach

Several tests close db; AfterEach closes it again. If Close isn’t idempotent, teardown will fail.

Run to confirm Close behavior and references:

If Close isn’t idempotent, prefer creating a separate temp DB instance within each test solely to close, or refactor to a helper returning an already-closed *Postgres that doesn’t affect the shared db used in AfterEach.

Also applies to: 201-215, 216-230


🏁 Script executed:

#!/bin/bash
# Inspect Close() semantics and usages
rg -nP --type=go 'type\s+Postgres\s+struct' -C3
rg -nP --type=go 'func\s*\(\s*\*Postgres\s*\)\s*Close\s*\(' -C5
rg -nP --type=go 'AfterEach\(.*db\.Close' -n

Length of output: 545


🏁 Script executed:

#!/bin/bash
# Locate Close method on Postgres struct
rg -nP --type=go 'func\s*\(\s*\*Postgres\s*\)\s*Close' -C5 pkg/database/postgres/postgres.go

# Fallback: search for any Close() invocation in pkg/database/postgres
rg -nP --type=go 'Close\(\)' -C3 pkg/database/postgres

# Inspect pool Close calls to infer idempotency
rg -nP --type=go 'ReadPool\.Close' -C3 pkg
rg -nP --type=go 'WritePool\.Close' -C3 pkg

# Locate any AfterEach teardown calling db.Close in watch tests
rg -nP 'AfterEach' -C3 internal/storage/postgres/watch_test.go

Length of output: 3078


Do not close the shared db in this spec; use a separate closed instance
Postgres.Close() unconditionally calls ReadPool.Close() and WritePool.Close() (pkg/database/postgres/postgres.go:137–141), so closing the shared db in the test then again in AfterEach risks double-close panics or silent failures. Instead, for the “SQL builder error” case, instantiate a dedicated *Postgres (or use the MockPQDatabase) that’s already closed, and leave the shared db untouched.

🤖 Prompt for AI Agents
internal/storage/postgres/watch_test.go around lines 186 to 200: the test
currently closes the shared `db` to simulate an SQL builder error; instead,
create a new dedicated `*Postgres` (or use the MockPQDatabase) instance, call
Close() on that dedicated instance, and pass it to NewWatcher so the shared `db`
remains untouched; update the spec to use this closed dedicated instance for
watcherWithClosedDB and remove any closing of the shared `db` in this test to
avoid double-close issues.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

♻️ Duplicate comments (1)
internal/authn/oidc/authn_test.go (1)

679-733: Assert true cancellation and shorten backoff to reduce flakiness

Tighten the assertion to require cancellation (don’t accept generic token errors) and increase test determinism by shortening backoff frequency. This also speeds the test.

-            auth, err := NewOidcAuthn(ctx, config.Oidc{
+            auth, err := NewOidcAuthn(ctx, config.Oidc{
                 Audience:          audience,
                 Issuer:            issuerURL,
                 RefreshInterval:   5 * time.Minute,
                 BackoffInterval:   12 * time.Second,
                 BackoffMaxRetries: 3,
-                BackoffFrequency:  2 * time.Second, // Longer frequency to allow cancellation
+                BackoffFrequency:  200 * time.Millisecond,
             })
@@
-            time.Sleep(100 * time.Millisecond)
+            time.Sleep(300 * time.Millisecond)
             cancel()
@@
-            case err := <-authErr:
-                Expect(err).To(HaveOccurred())
-                // The error might be wrapped, so check for context cancellation in the error chain
-                Expect(err.Error()).To(Or(
-                    ContainSubstring("context canceled"),
-                    ContainSubstring("context cancelled"),
-                    Equal(base.ErrorCode_ERROR_CODE_INVALID_BEARER_TOKEN.String()),
-                ))
+            case err := <-authErr:
+                Expect(err).To(HaveOccurred())
+                Expect(err.Error()).To(Or(
+                    ContainSubstring("context canceled"),
+                    ContainSubstring("context cancelled"),
+                ))
🧹 Nitpick comments (2)
internal/authn/oidc/authn_test.go (2)

524-602: Reduce brittleness in error assertions; consider DRYing the cases

Exact string equality on errors is brittle. Prefer substring match. Optionally, collapse these into a table-driven loop to avoid repetition.

Apply this minimal change to loosen assertions:

-            Expect(err.Error()).To(Equal("invalid or missing backoffInterval"))
+            Expect(err.Error()).To(ContainSubstring("invalid or missing backoffInterval"))

-            Expect(err.Error()).To(Equal("invalid or missing backoffInterval"))
+            Expect(err.Error()).To(ContainSubstring("invalid or missing backoffInterval"))

-            Expect(err.Error()).To(Equal("invalid or missing backoffMaxRetries"))
+            Expect(err.Error()).To(ContainSubstring("invalid or missing backoffMaxRetries"))

-            Expect(err.Error()).To(Equal("invalid or missing backoffMaxRetries"))
+            Expect(err.Error()).To(ContainSubstring("invalid or missing backoffMaxRetries"))

-            Expect(err.Error()).To(Equal("invalid or missing backoffFrequency"))
+            Expect(err.Error()).To(ContainSubstring("invalid or missing backoffFrequency"))

-            Expect(err.Error()).To(Equal("invalid or missing backoffFrequency"))
+            Expect(err.Error()).To(ContainSubstring("invalid or missing backoffFrequency"))

664-676: Prefer pending test (XIt/PIt) over runtime Skip

Marking the spec pending avoids executing the body and communicates intent more clearly.

-        It("should return error for invalid token claims format", func() {
+        XIt("should return error for invalid token claims format", func() {
@@
-            Skip("Invalid claims format test - JWT library automatically converts to MapClaims")
         })
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between d8de596 and fdabe53.

📒 Files selected for processing (1)
  • internal/authn/oidc/authn_test.go (3 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
internal/authn/oidc/authn_test.go (2)
internal/authn/oidc/authn.go (1)
  • NewOidcAuthn (53-111)
internal/config/config.go (1)
  • Oidc (72-80)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (3)
  • GitHub Check: Test with Coverage
  • GitHub Check: Analyze (go)
  • GitHub Check: Test with Coverage
🔇 Additional comments (4)
internal/authn/oidc/authn_test.go (4)

5-8: LGTM: imports

Both json and http are used by new negative-path tests; import additions are correct.


736-820: LGTM: robust OIDC discovery error coverage

Good negative-path tests for missing issuer/JWKs and invalid JSON.


822-858: LGTM: HTTP error handling tests

Covers non-200 and invalid issuer URL paths with clear assertions.


860-889: LGTM: guarded Hijacker-based read error simulation

Nice addition of the Hijacker capability check to avoid flakiness.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 10

🧹 Nitpick comments (13)
internal/engines/expand_test.go (3)

1583-1587: Avoid brittle exact error string comparisons

Comparing err.Error() to base.ErrorCode_ERROR_CODE_SCHEMA_NOT_FOUND.String() is fragile (wrapping/formatting may change). Prefer matching by code or using substring.

Apply:

-           Expect(err.Error()).Should(Equal(base.ErrorCode_ERROR_CODE_SCHEMA_NOT_FOUND.String()))
+           Expect(err.Error()).Should(ContainSubstring(base.ErrorCode_ERROR_CODE_SCHEMA_NOT_FOUND.String()))

If there’s a typed error exposing a code, compare codes directly instead of strings.

Also applies to: 1613-1615


1590-1604: “context cancellation” test doesn’t validate cancellation

The failure is due to missing schema, not cancellation. Either write a minimal schema so the code reaches a cancelable path, and assert context.Canceled/status, or drop/rename this test.

Also applies to: 1609-1615


1520-1520: Remove hardcoded source line references in comments

Inline “(line …)” notes will drift. They add maintenance noise without test value.

-           // Test entity definition read error (line 77)
+           // Test entity definition read error

(Repeat for other instances in this context.)

Also applies to: 1552-1552, 1576-1576, 1601-1601, 1618-1618, 1628-1628, 1644-1644, 1659-1659

internal/engines/lookup_test.go (3)

5016-5020: Don’t pass nil stream sink

LookupEntityStream receives a nil sink; the test relies on failing earlier. Pass a no-op sink to decouple from call-order.

-            err = engine.LookupEntityStream(context.Background(), request, nil)
+            sink := func(*base.PermissionLookupEntityStreamResponse) error { return nil }
+            err = engine.LookupEntityStream(context.Background(), request, sink)

24-46: Local mockSchemaReader OK; consider reuse

The stub is fine. If multiple packages need this, consider moving it to a shared test helper to reduce duplication.


4955-4958: Message substring check is fine; prefer sentinel error if available

Using “failed to create bulk checker” ties to wording. If the engine exposes a sentinel error, switch to errors.Is.

internal/engines/bulk_test.go (7)

63-65: Avoid brittle string equality for errors

Exact string matches are fragile. Prefer MatchError with substring.

-			Expect(err).Should(HaveOccurred())
-			Expect(err.Error()).Should(Equal("context cannot be nil"))
+			Expect(err).Should(MatchError(ContainSubstring("context cannot be nil")))
-			Expect(err).Should(HaveOccurred())
-			Expect(err.Error()).Should(Equal("callback cannot be nil"))
+			Expect(err).Should(MatchError(ContainSubstring("callback cannot be nil")))

Also applies to: 76-78


191-193: Same: use MatchError instead of string equality

-				Expect(err).Should(HaveOccurred())
-				Expect(err.Error()).Should(Equal("size must be greater than 0"))
+				Expect(err).Should(MatchError(ContainSubstring("size must be greater than 0")))

237-239: Fix misleading comment

This creates a checker whose Check returns an error; the callback isn’t returning an error.

-				// Create a checker with a callback that returns an error
+				// Create a checker whose underlying engine returns an error

124-186: Consider edge cases in sortRequests tests

Add cases for: already-sorted input (stability), duplicate IDs, and nil Entity/Subject to guard against panics.

I can add these tests if you want.


213-216: Strengthen success-path coverage

Currently ExecuteRequests(1) runs with no queued requests. Consider publishing at least one request (via a publisher or directly pushing to checker.requestChan) and asserting the callback is invoked.

I can draft a minimal test that enqueues one request and waits with Eventually on a callback counter.


48-103: Add missing validations: nil checker and negative config values

Expand NewBulkChecker tests to cover nil checker error and negative ConcurrencyLimit/BufferSize defaulting.

 	Context("NewBulkChecker", func() {
 		var mockChecker invoke.Check
 
 		BeforeEach(func() {
 			mockChecker = &mockCheckEngine{}
 		})
 
+		It("should return error for nil checker", func() {
+			ctx := context.Background()
+			config := BulkCheckerConfig{ConcurrencyLimit: 5, BufferSize: 100}
+			_, err := NewBulkChecker(ctx, nil, BulkCheckerTypeEntity, func(string, string) {}, config)
+			Expect(err).Should(MatchError(ContainSubstring("checker cannot be nil")))
+		})
+
+		It("should default negative config values", func() {
+			ctx := context.Background()
+			config := BulkCheckerConfig{ConcurrencyLimit: -1, BufferSize: -5}
+			chk, err := NewBulkChecker(ctx, mockChecker, BulkCheckerTypeEntity, func(string, string) {}, config)
+			Expect(err).ShouldNot(HaveOccurred())
+			DeferCleanup(chk.cancel)
+			Expect(chk.config.ConcurrencyLimit).Should(Equal(10))
+			Expect(chk.config.BufferSize).Should(Equal(1000))
+		})

41-45: Optional: don’t over-specify defaults

Asserting concrete numbers can cause churn if defaults tune over time. If the goal is “non-zero sensible defaults,” consider BeNumerically(">=", 1) instead.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between fdabe53 and ace258d.

📒 Files selected for processing (3)
  • internal/engines/bulk_test.go (1 hunks)
  • internal/engines/expand_test.go (1 hunks)
  • internal/engines/lookup_test.go (2 hunks)
🧰 Additional context used
🧬 Code graph analysis (3)
internal/engines/lookup_test.go (2)
pkg/pb/base/v1/service.pb.go (12)
  • PermissionLookupEntityRequest (505-531)
  • PermissionLookupEntityRequest (546-546)
  • PermissionLookupEntityRequest (561-563)
  • PermissionLookupEntityRequestMetadata (629-640)
  • PermissionLookupEntityRequestMetadata (655-655)
  • PermissionLookupEntityRequestMetadata (670-672)
  • PermissionLookupSubjectRequest (984-1009)
  • PermissionLookupSubjectRequest (1024-1024)
  • PermissionLookupSubjectRequest (1039-1041)
  • PermissionLookupSubjectRequestMetadata (1107-1118)
  • PermissionLookupSubjectRequestMetadata (1133-1133)
  • PermissionLookupSubjectRequestMetadata (1148-1150)
internal/engines/lookup.go (1)
  • NewLookupEngine (31-51)
internal/engines/bulk_test.go (2)
pkg/pb/base/v1/base.pb.go (2)
  • CheckResult_CHECK_RESULT_ALLOWED (35-35)
  • CheckResult_CHECK_RESULT_UNSPECIFIED (33-33)
internal/engines/bulk.go (9)
  • DefaultBulkCheckerConfig (56-61)
  • BulkCheckerConfig (44-52)
  • NewBulkChecker (130-170)
  • BulkCheckerTypeEntity (28-28)
  • BulkChecker (69-98)
  • BulkCheckerRequest (34-39)
  • BulkCheckerTypeSubject (26-26)
  • NewBulkEntityPublisher (497-502)
  • NewBulkSubjectPublisher (552-557)
internal/engines/expand_test.go (4)
pkg/pb/base/v1/base.pb.go (12)
  • Context (421-432)
  • Context (447-447)
  • Context (462-464)
  • Entity (1806-1813)
  • Entity (1828-1828)
  • Entity (1843-1845)
  • Expand (2279-2297)
  • Expand (2312-2312)
  • Expand (2327-2329)
  • Argument (1258-1267)
  • Argument (1282-1282)
  • Argument (1297-1299)
internal/engines/expand.go (3)
  • NewExpandEngine (30-35)
  • ExpandResponse (57-60)
  • ExpandFunction (64-64)
pkg/pb/base/v1/service.pb.go (3)
  • PermissionExpandRequest (304-321)
  • PermissionExpandRequest (336-336)
  • PermissionExpandRequest (351-353)
pkg/pb/base/v1/errors.pb.go (1)
  • ErrorCode_ERROR_CODE_SCHEMA_NOT_FOUND (68-68)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (3)
  • GitHub Check: Analyze (go)
  • GitHub Check: Test with Coverage
  • GitHub Check: Test with Coverage
🔇 Additional comments (3)
internal/engines/expand_test.go (1)

1518-1530: Name vs. behavior mismatch

Test name says “entity definition read errors” but the setup triggers “schema not found.” Either write a schema and request a non-existent entity to actually hit entity-def read, or rename.

internal/engines/lookup_test.go (1)

4928-5103: No action needed for mockCheckEngine or nil callbacks The references to mockCheckEngine in lookup_test.go and the nil arguments passed to LookupEntityStream are deliberate in these tests to simulate error conditions; ignore the original suggestion.

Likely an incorrect or invalid review comment.

internal/engines/bulk_test.go (1)

265-293: Leave ctx parameter in NewBulkEntityPublisher/NewBulkSubjectPublisher
The constructors accept a context but don’t store it by design (see bulk.go parameter docs at lines 491–492 and 546–547: “ctx: Context for the publisher (currently unused but kept for API consistency)”). No changes required.

Comment on lines +86 to +90
checker, err := NewBulkChecker(ctx, mockChecker, BulkCheckerTypeEntity, func(entityID, ct string) {}, config)

Expect(err).ShouldNot(HaveOccurred())
Expect(checker.config.ConcurrencyLimit).Should(Equal(10)) // Default value
})
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Cleanup created BulkChecker instances in tests

Use DeferCleanup to ensure cancellation even on failure, preventing leaks.

 			checker, err := NewBulkChecker(ctx, mockChecker, BulkCheckerTypeEntity, func(entityID, ct string) {}, config)
+			DeferCleanup(checker.cancel)
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
checker, err := NewBulkChecker(ctx, mockChecker, BulkCheckerTypeEntity, func(entityID, ct string) {}, config)
Expect(err).ShouldNot(HaveOccurred())
Expect(checker.config.ConcurrencyLimit).Should(Equal(10)) // Default value
})
checker, err := NewBulkChecker(ctx, mockChecker, BulkCheckerTypeEntity, func(entityID, ct string) {}, config)
DeferCleanup(checker.cancel)
Expect(err).ShouldNot(HaveOccurred())
Expect(checker.config.ConcurrencyLimit).Should(Equal(10)) // Default value
})
🤖 Prompt for AI Agents
In internal/engines/bulk_test.go around lines 86 to 90, register a DeferCleanup
right after creating the checker to ensure it gets cancelled on test failure:
after NewBulkChecker returns, call DeferCleanup(func(){ if checker != nil {
checker.Cancel() } }) (or checker.Close()/checker.Stop() if that is the actual
teardown method on BulkChecker) so the checker is cleaned up even when
assertions fail.

Comment on lines +98 to +102
checker, err := NewBulkChecker(ctx, mockChecker, BulkCheckerTypeEntity, func(entityID, ct string) {}, config)

Expect(err).ShouldNot(HaveOccurred())
Expect(checker.config.BufferSize).Should(Equal(1000)) // Default value
})
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Add DeferCleanup here as well

 			checker, err := NewBulkChecker(ctx, mockChecker, BulkCheckerTypeEntity, func(entityID, ct string) {}, config)
+			DeferCleanup(checker.cancel)
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
checker, err := NewBulkChecker(ctx, mockChecker, BulkCheckerTypeEntity, func(entityID, ct string) {}, config)
Expect(err).ShouldNot(HaveOccurred())
Expect(checker.config.BufferSize).Should(Equal(1000)) // Default value
})
checker, err := NewBulkChecker(ctx, mockChecker, BulkCheckerTypeEntity, func(entityID, ct string) {}, config)
DeferCleanup(checker.cancel)
Expect(err).ShouldNot(HaveOccurred())
Expect(checker.config.BufferSize).Should(Equal(1000)) // Default value
})
🤖 Prompt for AI Agents
In internal/engines/bulk_test.go around lines 98 to 102, the test creates a
BulkChecker but does not register a cleanup, so add a DeferCleanup call after
creation to ensure the checker is torn down; e.g., immediately after
NewBulkChecker succeeds call DeferCleanup with the checker’s cleanup method (for
example DeferCleanup(checker.Close) or DeferCleanup(checker.Stop) depending on
the BulkChecker API) so resources are released after the test.

Comment on lines +110 to +123
BeforeEach(func() {
ctx = context.Background()
config := BulkCheckerConfig{
ConcurrencyLimit: 2,
BufferSize: 10,
}

mockChecker = &mockCheckEngine{}

var err error
checker, err = NewBulkChecker(ctx, mockChecker, BulkCheckerTypeEntity, func(entityID, ct string) {}, config)
Expect(err).ShouldNot(HaveOccurred())
})

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Cancel the BulkChecker context to avoid goroutine leaks

NewBulkChecker starts a background collector; without canceling, tests can leak goroutines. Add an AfterEach that calls checker.cancel().

 		BeforeEach(func() {
 			ctx = context.Background()
 			config := BulkCheckerConfig{
 				ConcurrencyLimit: 2,
 				BufferSize:       10,
 			}
 
 			mockChecker = &mockCheckEngine{}
 
 			var err error
 			checker, err = NewBulkChecker(ctx, mockChecker, BulkCheckerTypeEntity, func(entityID, ct string) {}, config)
 			Expect(err).ShouldNot(HaveOccurred())
 		})
+
+		AfterEach(func() {
+			if checker != nil && checker.cancel != nil {
+				checker.cancel()
+			}
+		})
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
BeforeEach(func() {
ctx = context.Background()
config := BulkCheckerConfig{
ConcurrencyLimit: 2,
BufferSize: 10,
}
mockChecker = &mockCheckEngine{}
var err error
checker, err = NewBulkChecker(ctx, mockChecker, BulkCheckerTypeEntity, func(entityID, ct string) {}, config)
Expect(err).ShouldNot(HaveOccurred())
})
BeforeEach(func() {
ctx = context.Background()
config := BulkCheckerConfig{
ConcurrencyLimit: 2,
BufferSize: 10,
}
mockChecker = &mockCheckEngine{}
var err error
checker, err = NewBulkChecker(ctx, mockChecker, BulkCheckerTypeEntity, func(entityID, ct string) {}, config)
Expect(err).ShouldNot(HaveOccurred())
})
AfterEach(func() {
if checker != nil && checker.cancel != nil {
checker.cancel()
}
})
🤖 Prompt for AI Agents
In internal/engines/bulk_test.go around lines 110 to 123, the test's BeforeEach
creates a BulkChecker that starts a background collector but the context is
never canceled; add an AfterEach that calls checker.cancel() (or checker.cancel
if it's a method/field) to ensure the background goroutine is stopped; guard the
call with a nil check (e.g., if checker != nil { checker.cancel() }) to avoid
panics and place the AfterEach directly after the BeforeEach in the same
Describe/Context block.

Comment on lines +158 to +161
mockChecker := &mockCheckEngine{}
subjectChecker, err := NewBulkChecker(ctx, mockChecker, BulkCheckerTypeSubject, func(entityID, ct string) {}, config)
Expect(err).ShouldNot(HaveOccurred())

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Defer cleanup for subjectChecker

 				subjectChecker, err := NewBulkChecker(ctx, mockChecker, BulkCheckerTypeSubject, func(entityID, ct string) {}, config)
 				Expect(err).ShouldNot(HaveOccurred())
+				DeferCleanup(subjectChecker.cancel)
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
mockChecker := &mockCheckEngine{}
subjectChecker, err := NewBulkChecker(ctx, mockChecker, BulkCheckerTypeSubject, func(entityID, ct string) {}, config)
Expect(err).ShouldNot(HaveOccurred())
mockChecker := &mockCheckEngine{}
subjectChecker, err := NewBulkChecker(ctx, mockChecker, BulkCheckerTypeSubject, func(entityID, ct string) {}, config)
Expect(err).ShouldNot(HaveOccurred())
DeferCleanup(subjectChecker.cancel)
🤖 Prompt for AI Agents
In internal/engines/bulk_test.go around lines 158 to 161, the test creates
subjectChecker via NewBulkChecker but does not schedule cleanup; add a deferred
cleanup call immediately after the successful creation (e.g., defer
subjectChecker.Close()) so the checker is shut down at test end and resources
are released.

Comment on lines +205 to +207
cancelledChecker, err := NewBulkChecker(cancelCtx, mockChecker, BulkCheckerTypeEntity, func(entityID, ct string) {}, config)
Expect(err).ShouldNot(HaveOccurred())

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Defer cleanup for cancelledChecker (entity path)

 				cancelledChecker, err := NewBulkChecker(cancelCtx, mockChecker, BulkCheckerTypeEntity, func(entityID, ct string) {}, config)
 				Expect(err).ShouldNot(HaveOccurred())
+				DeferCleanup(cancelledChecker.cancel)
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
cancelledChecker, err := NewBulkChecker(cancelCtx, mockChecker, BulkCheckerTypeEntity, func(entityID, ct string) {}, config)
Expect(err).ShouldNot(HaveOccurred())
cancelledChecker, err := NewBulkChecker(cancelCtx, mockChecker, BulkCheckerTypeEntity, func(entityID, ct string) {}, config)
Expect(err).ShouldNot(HaveOccurred())
DeferCleanup(cancelledChecker.cancel)
🤖 Prompt for AI Agents
In internal/engines/bulk_test.go around lines 205–207, the test creates
cancelledChecker but does not defer its cleanup; immediately after the
NewBulkChecker call add a deferred cleanup call (e.g., defer
cancelledChecker.Close() or defer cancelledChecker.Stop() using the actual
cleanup method on BulkChecker) to ensure resources are released even if the test
fails.

Comment on lines +245 to +247
errorBulkChecker, err := NewBulkChecker(ctx, errorChecker, BulkCheckerTypeEntity, func(entityID, ct string) {}, config)
Expect(err).ShouldNot(HaveOccurred())

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Defer cleanup for errorBulkChecker

 				errorBulkChecker, err := NewBulkChecker(ctx, errorChecker, BulkCheckerTypeEntity, func(entityID, ct string) {}, config)
 				Expect(err).ShouldNot(HaveOccurred())
+				DeferCleanup(errorBulkChecker.cancel)
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
errorBulkChecker, err := NewBulkChecker(ctx, errorChecker, BulkCheckerTypeEntity, func(entityID, ct string) {}, config)
Expect(err).ShouldNot(HaveOccurred())
errorBulkChecker, err := NewBulkChecker(ctx, errorChecker, BulkCheckerTypeEntity, func(entityID, ct string) {}, config)
Expect(err).ShouldNot(HaveOccurred())
DeferCleanup(errorBulkChecker.cancel)
🤖 Prompt for AI Agents
internal/engines/bulk_test.go around lines 245 to 247: the test creates
errorBulkChecker but doesn't clean it up; add a defer immediately after creation
to call its cleanup method (e.g., defer errorBulkChecker.Close()) or, if the
type exposes Stop/Shutdown, call that instead (wrap with a nil-check or ignore
the returned error with _ = ... to avoid test failures).

Comment on lines +278 to +280
cancelledChecker, err := NewBulkChecker(cancelCtx, mockChecker, BulkCheckerTypeEntity, func(entityID, ct string) {}, config)
Expect(err).ShouldNot(HaveOccurred())

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Defer cleanup for cancelledChecker (entity publisher)

 			cancelledChecker, err := NewBulkChecker(cancelCtx, mockChecker, BulkCheckerTypeEntity, func(entityID, ct string) {}, config)
 			Expect(err).ShouldNot(HaveOccurred())
+			DeferCleanup(cancelledChecker.cancel)
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
cancelledChecker, err := NewBulkChecker(cancelCtx, mockChecker, BulkCheckerTypeEntity, func(entityID, ct string) {}, config)
Expect(err).ShouldNot(HaveOccurred())
cancelledChecker, err := NewBulkChecker(cancelCtx, mockChecker, BulkCheckerTypeEntity, func(entityID, ct string) {}, config)
Expect(err).ShouldNot(HaveOccurred())
DeferCleanup(cancelledChecker.cancel)
🤖 Prompt for AI Agents
In internal/engines/bulk_test.go around lines 278 to 280, the created
cancelledChecker (entity publisher) is not cleaned up; add a defer to close it
immediately after creation to avoid resource leakage: after the
Expect(err).ShouldNot(HaveOccurred()) line, add defer cancelledChecker.Close()
(or the appropriate teardown method such as Stop()/Shutdown() if Close() is not
available) so the checker is properly stopped at test end.

Comment on lines +308 to +310
cancelledChecker, err := NewBulkChecker(cancelCtx, mockChecker, BulkCheckerTypeSubject, func(entityID, ct string) {}, config)
Expect(err).ShouldNot(HaveOccurred())

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Defer cleanup for cancelledChecker (subject publisher)

 			cancelledChecker, err := NewBulkChecker(cancelCtx, mockChecker, BulkCheckerTypeSubject, func(entityID, ct string) {}, config)
 			Expect(err).ShouldNot(HaveOccurred())
+			DeferCleanup(cancelledChecker.cancel)
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
cancelledChecker, err := NewBulkChecker(cancelCtx, mockChecker, BulkCheckerTypeSubject, func(entityID, ct string) {}, config)
Expect(err).ShouldNot(HaveOccurred())
cancelledChecker, err := NewBulkChecker(cancelCtx, mockChecker, BulkCheckerTypeSubject, func(entityID, ct string) {}, config)
Expect(err).ShouldNot(HaveOccurred())
DeferCleanup(cancelledChecker.cancel)
🤖 Prompt for AI Agents
In internal/engines/bulk_test.go around lines 308 to 310, the newly created
cancelledChecker (subject publisher) is not cleaned up; add a deferred close
after creation to release resources: immediately after the
Expect(err).ShouldNot(HaveOccurred()) line, add defer cancelledChecker.Close()
(or the appropriate teardown method on BulkChecker such as Stop/Shutdown if
Close() does not exist) so the subject publisher is properly cleaned up at test
end.

Comment on lines +1659 to +1667
// Test expandIntersection function (line 853)
entity := &base.Entity{Type: "user", Id: "user1"}
permission := "read"
arguments := []*base.Argument{}
functions := []ExpandFunction{}

response := expandIntersection(context.Background(), entity, permission, arguments, functions)
Expect(response).ShouldNot(BeNil())
})
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Test always passes; struct value can’t be nil

expandIntersection returns a value (not a pointer). Expect(response).ShouldNot(BeNil()) will always succeed. Assert on fields instead.

Apply:

-           response := expandIntersection(context.Background(), entity, permission, arguments, functions)
-           Expect(response).ShouldNot(BeNil())
+           resp := expandIntersection(context.Background(), entity, permission, arguments, functions)
+           Expect(resp.Err).ShouldNot(HaveOccurred())
+           Expect(resp.Response).ShouldNot(BeNil())
+           Expect(resp.Response.Tree).ShouldNot(BeNil())
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// Test expandIntersection function (line 853)
entity := &base.Entity{Type: "user", Id: "user1"}
permission := "read"
arguments := []*base.Argument{}
functions := []ExpandFunction{}
response := expandIntersection(context.Background(), entity, permission, arguments, functions)
Expect(response).ShouldNot(BeNil())
})
// Test expandIntersection function (line 853)
entity := &base.Entity{Type: "user", Id: "user1"}
permission := "read"
arguments := []*base.Argument{}
functions := []ExpandFunction{}
resp := expandIntersection(context.Background(), entity, permission, arguments, functions)
Expect(resp.Err).ShouldNot(HaveOccurred())
Expect(resp.Response).ShouldNot(BeNil())
Expect(resp.Response.Tree).ShouldNot(BeNil())
})
🤖 Prompt for AI Agents
In internal/engines/expand_test.go around lines 1659 to 1667, the test calls
expandIntersection which returns a non-pointer struct, so
Expect(response).ShouldNot(BeNil()) always succeeds; change the assertion to
validate the struct's contents instead. Replace the nil check with assertions
against meaningful fields (for example, check response.Error is nil/empty,
response.Permissions length, or specific expected values in response) or use
deep equality to an expected struct instance; ensure you construct the expected
result or assert on concrete fields that prove expandIntersection behaved
correctly.

Comment on lines +4972 to +4974
mockCheckEngine := &mockCheckEngine{}
engine := NewLookupEngine(mockCheckEngine, schemaReader, dataReader)

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Undefined mockCheckEngine will break build

mockCheckEngine isn’t defined in this file. Use the real CheckEngine to avoid compile issues and still exercise the readSchema error path.

-            mockCheckEngine := &mockCheckEngine{}
-            engine := NewLookupEngine(mockCheckEngine, schemaReader, dataReader)
+            checkEngine := NewCheckEngine(schemaReader, dataReader)
+            engine := NewLookupEngine(checkEngine, schemaReader, dataReader)

Apply the same replacement in all four tests.

Also applies to: 5034-5036, 5065-5067, 5097-5099

🤖 Prompt for AI Agents
internal/engines/lookup_test.go lines 4972-4974 (and similarly 5034-5036,
5065-5067, 5097-5099): the tests instantiate an undefined mockCheckEngine which
will break the build; replace each mockCheckEngine instantiation with a real
CheckEngine (construct it with its required dependencies) so the tests compile
and still exercise the readSchema error path, applying the same replacement in
all four test locations.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants