Skip to content

Implement DB-backed user types with tests (3.5.3)#322

Merged
leynos merged 29 commits intomainfrom
db-backed-user-profiles-3-5-3-lce3o6
Mar 7, 2026
Merged

Implement DB-backed user types with tests (3.5.3)#322
leynos merged 29 commits intomainfrom
db-backed-user-profiles-3-5-3-lce3o6

Conversation

@leynos
Copy link
Copy Markdown
Owner

@leynos leynos commented Mar 4, 2026

Summary

  • Adds an Execution Plan document for roadmap item 3.5.3 to replace fixture-backed wiring with DB-backed adapters for UserProfileQuery and UserInterestsCommand.
  • Clarifies the chosen adapter strategy (dedicated DB adapters by default with explicit option to use repository extensions) and documents fixture fallback when DB is not configured.
  • Minor test formatting normalization in a frontend test to align with test harness conventions.

Changes

Documentation

  • Added docs/execplans/backend-3-5-3-db-backed-user-types.md describing goals, constraints, risks, progress, and plan for 3.5.3.

Code

  • Minor formatting improvement in frontend-pwa/scripts/run-audit.test.mjs (parameterized test indentation).

Tests

  • This PR provides the scaffolding and plan; actual DB-backed adapters and tests will be introduced in subsequent PRs.

Rationale

  • Aligns with hexagonal architecture: startup wiring can switch between fixture-backed ports and explicit DB-backed ports depending on config.db_pool.
  • Keeps endpoints stable while enabling DB-backed behaviour behind a feature gate (db_pool presence).

Implementation plan (per ExecPlan)

  • Stage A: lock the strategy and write/expand red tests first.
  • Stage B: add outbound concrete types and wire them in state_builders when db_pool is present.
  • Stage C: expand behavioral coverage with rstest/rstest-bdd tests.
  • Stage D: document architecture decision and close roadmap item with gates.

Validation and acceptance

  • DB-present startup uses DB-backed concrete adapters; DB-absent uses fixture adapters.
  • Tests: rstest and rstest-bdd cover happy, unhappy, and edge cases.
  • Full gates (make check-fmt, make lint, make test) pass with logs retained.

How to test locally

  • Review the new ExecPlan doc at docs/execplans/backend-3-5-3-db-backed-user-types.md.
  • Run repository gates:
    • make check-fmt
    • make lint
    • make test

Artifacts

  • New doc location: docs/execplans/backend-3-5-3-db-backed-user-types.md

Notes

  • This PR does not change runtime API or introduce new migrations; it prepares the ground for 3.5.3 implementation.

◳ Generated by DevBoxer


ℹ️ Tag @devboxerhub to ask questions and address PR feedback

📎 Task: https://www.devboxer.com/task/d72b1dd4-5d4d-42ba-8f79-68826a3d6e5c

Summary by Sourcery

Switch user profile and interests endpoints from fixture-only wiring to DB-backed adapters when a database pool is configured, while preserving fixture fallbacks and documenting the chosen adapter strategy.

New Features:

  • Introduce Diesel-backed adapters for UserProfileQuery and UserInterestsCommand and wire them into HTTP state when a DB pool is present.
  • Add BDD feature scenarios and integration flows covering profile and interests behaviour under DB-present startup modes.

Enhancements:

  • Refine backend architecture docs with an explicit decision to use dedicated profile/interests adapters instead of extending the user repository.
  • Update the backend roadmap to mark profile/interests DB wiring item 3.5.3 as complete and adjust follow-on items for future hardening and coverage.
  • Ensure startup composition for user state now consistently selects DB-backed or fixture adapters for login, users, profile, and interests based on configuration.

Documentation:

  • Add an execution plan document for roadmap item 3.5.3 describing constraints, risks, plan, and outcomes for DB-backed user types.

Tests:

  • Add focused integration tests for profile/interests routes covering fixture-fallback versus DB-backed behaviour and persistence of interests.
  • Add rstest-bdd scenarios validating DB-present startup behaviour when interests schema is missing and when interests validation errors occur.

…eplacing fixture-backed user types with DB-backed adapters

Co-authored-by: devboxerhub[bot] <devboxerhub[bot]@users.noreply.github.com>
@sourcery-ai
Copy link
Copy Markdown

sourcery-ai Bot commented Mar 4, 2026

Reviewer's Guide

Implements DB-backed adapters for user profile and interests, wires them into server startup with config-based DB/fixture switching, adds integration and BDD tests for startup modes and validation/error behavior, updates architecture and roadmap docs, and introduces a small new frontend dependency and formatting-normalization in tests.

Sequence diagram for startup wiring of profile and interests adapters

sequenceDiagram
    participant Main as main
    participant ServerConfig as ServerConfig
    participant StateBuilders as state_builders
    participant HttpState as HttpState
    participant ProfilePort as UserProfileQuery
    participant InterestsPort as UserInterestsCommand
    participant DieselProfile as DieselUserProfileQuery
    participant DieselInterests as DieselUserInterestsCommand
    participant FixtureProfile as FixtureUserProfileQuery
    participant FixtureInterests as FixtureUserInterestsCommand

    Main->>ServerConfig: load()
    Main->>StateBuilders: build_http_state(config)
    activate StateBuilders

    StateBuilders->>StateBuilders: build_login_users_pair(config)
    StateBuilders->>StateBuilders: build_profile_interests_pair(config)

    alt db_pool present
        StateBuilders->>DieselProfile: new(DieselUserRepository)
        StateBuilders->>DieselInterests: new(DieselUserPreferencesRepository)
        StateBuilders-->>ProfilePort: Arc~DieselUserProfileQuery~
        StateBuilders-->>InterestsPort: Arc~DieselUserInterestsCommand~
    else db_pool absent
        StateBuilders->>FixtureProfile: construct
        StateBuilders->>FixtureInterests: construct
        StateBuilders-->>ProfilePort: Arc~FixtureUserProfileQuery~
        StateBuilders-->>InterestsPort: Arc~FixtureUserInterestsCommand~
    end

    StateBuilders-->>HttpState: HttpStatePorts{login,users,profile,interests,...}
    deactivate StateBuilders

    Main-->>HttpState: start_http_server(HttpState)
Loading

Class diagram for new Diesel user profile and interests adapters

classDiagram
    class UserProfileQuery {
        <<interface>>
        +fetch_profile(user_id: UserId) Result~User, Error~
    }

    class UserInterestsCommand {
        <<interface>>
        +set_interests(user_id: UserId, interest_theme_ids: Vec~InterestThemeId~) Result~UserInterests, Error~
    }

    class UserRepository {
        <<interface>>
        +upsert(user: User) Result~(), UserPersistenceError~
        +find_by_id(id: UserId) Result~Option~User~, UserPersistenceError~
    }

    class UserPreferencesRepository {
        <<interface>>
        +find_by_user_id(user_id: UserId) Result~Option~UserPreferences~, UserPreferencesRepositoryError~
        +save(preferences: UserPreferences, expected_revision: Option~u32~) Result~(), UserPreferencesRepositoryError~
    }

    class DieselUserRepository {
        +new(pool: DbPool)
    }

    class DieselUserPreferencesRepository {
        +new(pool: DbPool)
    }

    class DieselUserProfileQuery {
        -user_repository: Arc~UserRepository~
        +new(user_repository: DieselUserRepository)
        +fetch_profile(user_id: UserId) Result~User, Error~
    }

    class DieselUserInterestsCommand {
        -preferences_repository: Arc~UserPreferencesRepository~
        +new(preferences_repository: DieselUserPreferencesRepository)
        +set_interests(user_id: UserId, interest_theme_ids: Vec~InterestThemeId~) Result~UserInterests, Error~
    }

    class UserPreferencesRepositoryError {
        <<enum>>
        Connection
        Query
        RevisionMismatch
    }

    class UserPersistenceError {
        <<enum>>
        Connection
        Query
    }

    class Error {
        +code() ErrorCode
        +message() String
        +service_unavailable(message: String) Error
        +internal(message: String) Error
    }

    class ErrorCode {
        <<enum>>
        ServiceUnavailable
        InternalError
    }

    class User {
        +id() UserId
    }

    class UserId {
        +as_ref() &str
    }

    class InterestThemeId {
        +as_uuid() &Uuid
    }

    class UserInterests {
        +new(user_id: UserId, interest_theme_ids: Vec~InterestThemeId~) UserInterests
        +user_id() &UserId
        +interest_theme_ids() &Vec~InterestThemeId~
    }

    class UserPreferences {
        +user_id: UserId
        +interest_theme_ids: Vec~Uuid~
        +safety_toggle_ids: Vec~Uuid~
        +unit_system: UnitSystem
        +revision: u32
        +builder(user_id: UserId) UserPreferencesBuilder
    }

    class UserPreferencesBuilder {
        +interest_theme_ids(ids: Vec~Uuid~) UserPreferencesBuilder
        +safety_toggle_ids(ids: Vec~Uuid~) UserPreferencesBuilder
        +unit_system(unit_system: UnitSystem) UserPreferencesBuilder
        +revision(revision: u32) UserPreferencesBuilder
        +build() UserPreferences
    }

    class UnitSystem {
        <<enum>>
        Metric
        Imperial
    }

    UserProfileQuery <|.. DieselUserProfileQuery
    UserInterestsCommand <|.. DieselUserInterestsCommand

    UserRepository <|.. DieselUserRepository
    UserPreferencesRepository <|.. DieselUserPreferencesRepository

    DieselUserProfileQuery --> UserRepository : uses
    DieselUserInterestsCommand --> UserPreferencesRepository : uses

    UserPreferencesRepositoryError --> Error : mapped_by
    UserPersistenceError --> Error : mapped_by

    UserPreferences --> UnitSystem
    UserPreferences --> UserId
    UserPreferences --> "*" Uuid

    UserInterests --> UserId
    UserInterests --> InterestThemeId
Loading

File-Level Changes

Change Details Files
Add Diesel-backed adapters for UserProfileQuery and UserInterestsCommand and expose them via the outbound persistence module.
  • Implement DieselUserProfileQuery on top of UserRepository with proper error mapping and missing-user handling.
  • Implement DieselUserInterestsCommand on top of UserPreferencesRepository, including revision-aware updates and domain-error mapping.
  • Export the new adapters from the outbound persistence mod so they can be used by server wiring.
backend/src/outbound/persistence/diesel_user_profile_query.rs
backend/src/outbound/persistence/diesel_user_interests_command.rs
backend/src/outbound/persistence/mod.rs
Wire profile and interests ports to choose DB-backed adapters when a db_pool is configured, while preserving fixture-based startup behavior otherwise.
  • Introduce a build_profile_interests_pair helper to construct UserProfileQuery and UserInterestsCommand adapters based on ServerConfig.db_pool.
  • Update build_http_state to use the new helper and pass the selected profile/interests ports into HttpState.
  • Remove the obsolete TODO about future profile/interests wiring.
backend/src/server/state_builders.rs
Add integration and BDD-style tests to cover profile/interests behavior across DB-present and fixture-fallback startup modes, including validation and schema-loss scenarios.
  • Create diesel_profile_interests_adapters integration test to exercise login, profile fetch, and interests update flows in fixture and DB modes, including DB seeding and persistence checks.
  • Create user_state_profile_interests_startup_modes_bdd.rs and accompanying .feature file to validate behavior under DB-present mode, schema loss, and interests-validation edge cases with rstest-bdd.
  • Reuse existing embedded Postgres test support (cluster management, template provisioning, error formatting, and table dropping) in the new tests.
backend/tests/diesel_profile_interests_adapters.rs
backend/tests/user_state_profile_interests_startup_modes_bdd.rs
backend/tests/features/user_state_profile_interests_startup_modes.feature
Document the execution plan and architectural decision for DB-backed user types and update the backend roadmap state.
  • Add a detailed execution plan for roadmap item 3.5.3, covering constraints, risks, plan stages, validation, and evidence expectations.
  • Mark roadmap item 3.5.3 as completed and adjust subsequent items 3.5.5–3.5.6 to reflect the new responsibilities and focus on hardening and regression coverage.
  • Record in the architecture doc that dedicated outbound adapters were chosen over repository extensions, and describe startup behavior for DB-present vs fixture modes.
docs/execplans/backend-3-5-3-db-backed-user-types.md
docs/backend-roadmap.md
docs/wildside-backend-architecture.md
Adjust frontend dependencies and normalize test formatting in the PWA audit test suite.
  • Add dompurify as a new dependency in package.json and lockfile.
  • Tidy parameterized test indentation/formatting in the frontend PWA audit test script to match project conventions.
package.json
pnpm-lock.yaml
frontend-pwa/scripts/run-audit.test.mjs

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Mar 4, 2026

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 1e3fc230-3a25-4826-8b26-f48217b03559

📥 Commits

Reviewing files that changed from the base of the PR and between 4c90e09 and 8605be9.

📒 Files selected for processing (1)
  • backend/src/outbound/persistence/diesel_user_interests_command/tests/retry.rs

Summary by CodeRabbit

Release Notes

  • New Features

    • Database-backed profile and interests management with dedicated adapters.
  • Improvements

    • Added optimistic concurrency control with revision constraints and conflict detection for safe concurrent writes.
  • Tests

    • Comprehensive integration and BDD test coverage for profile/interests flows with database and fixture-fallback modes.
  • Documentation

    • Updated architecture documentation and execution plans documenting profile/interests wiring.
  • Chores

    • Dependency updates.

Walkthrough

Add Diesel-backed UserProfileQuery and UserInterestsCommand, wire them into HTTP state when a DB pool exists, implement optimistic-revision insert/update with retry and new repository errors, add non‑negative revision migrations, and add unit, integration and BDD tests plus test helpers and docs.

Changes

Cohort / File(s) Summary
Outbound Persistence Adapters
backend/src/outbound/persistence/diesel_user_interests_command.rs, backend/src/outbound/persistence/diesel_user_profile_query.rs, backend/src/outbound/persistence/mod.rs
Add DieselUserInterestsCommand and DieselUserProfileQuery with public constructors and trait impls; implement preferences update builder, optimistic-revision save with retry, error mapping, and re-export adapters.
Preferences Repository Concurrency
backend/src/outbound/persistence/diesel_user_preferences_repository.rs
Add revision casting helpers, fetch_current_revision, insert-conflict and update-failure handlers, and DB casting helpers; change row mapping to return Result and surface revision-related repository errors.
Server State Wiring
backend/src/server/state_builders.rs
Add build_profile_interests_pair_with_pool and build_profile_interests_pair; select Diesel adapters when a DB pool is present and wire them into HTTP state.
Interests Adapter Tests & Support
backend/src/outbound/persistence/diesel_user_interests_command/tests.rs, .../tests/mapping.rs, .../tests/retry.rs, .../tests/support.rs, .../tests/fixture_guards.rs
Add regression tests for mapping, retry behaviour and fixture guards; provide in-memory stub repositories, retry simulators and helpers to assert saved preferences, expected_revision handling and error mapping.
Integration Tests: profile & interests
backend/tests/diesel_profile_interests_adapters.rs
Add end-to-end Actix tests covering login→profile→interests flows for fixture and DB-backed modes, including DB seeding and persistence assertions.
BDD Feature & Glue
backend/tests/features/user_state_profile_interests_startup_modes.feature, backend/tests/user_state_profile_interests_startup_modes_bdd.rs
Add feature file and BDD-style tests validating startup-mode selection, missing-schema resilience, validation envelopes and response stability.
BDD Flow Test Support
backend/tests/user_state_profile_interests_startup_modes_bdd/flow_support.rs
Add test harness for login/profile/interests flows, embedded Postgres setup, snapshot capture, DB seeding and orchestration helpers.
Integration Test Helpers
backend/tests/support/mod.rs, backend/tests/support/profile_interests.rs
Add profile_interests helpers, shared constants and build_session_middleware() for integration tests.
Schema Migrations & Schema Tests
backend/migrations/..._add_non_negative_revision_checks/up.sql, .../down.sql, backend/tests/diesel_user_preferences_repository.rs, backend/tests/diesel_route_annotation_repository.rs
Add non‑negative revision constraints for user_preferences and route_notes; add integration tests asserting DB rejects negative revisions.
Preferences Port Errors & Service Mapping
backend/src/domain/ports/user_preferences_repository.rs, backend/src/domain/preferences_service.rs, backend/src/domain/preferences_service_tests.rs
Add MissingForUpdate{expected} and ConcurrentWriteConflict repository errors; map them to domain Conflict metadata and extend tests for mapping behaviour.
Docs & Execution Plan
docs/backend-roadmap.md, docs/execplans/backend-3-5-3-db-backed-user-types.md, docs/wildside-backend-architecture.md
Document completion and execution plan for DB-backed user adapters, wiring choices, test strategy and architecture rationale; record decision to use dedicated adapters.
Misc Test Scaffolding
backend/src/outbound/persistence/diesel_user_interests_command/tests/*
Add test scaffolding modules and helpers (mapping, retry, support) for interests adapter regression coverage.
Dependency Overrides
package.json
Add pnpm override entry for dompurify@3.3.2.

Sequence Diagram(s)

sequenceDiagram
    participant Client
    participant Server as HTTP Server
    participant State as AppState
    participant ProfileQ as DieselUserProfileQuery
    participant InterestsC as DieselUserInterestsCommand
    participant PrefRepo as UserPreferencesRepository
    participant DB as Database

    Client->>Server: POST /api/v1/login (credentials)
    Server->>State: authenticate user, create session
    Client->>Server: GET /api/v1/users/me
    Server->>State: fetch_profile(user_id)
    State->>ProfileQ: fetch_profile(user_id)
    ProfileQ->>DB: SELECT users WHERE id=...
    DB-->>ProfileQ: row / none / error
    ProfileQ-->>State: User / error
    Client->>Server: PUT /api/v1/users/me/interests (theme IDs)
    Server->>State: set_interests(user_id, themes)
    State->>InterestsC: set_interests(user_id, themes)
    InterestsC->>PrefRepo: find_by_user_id(user_id)
    PrefRepo->>DB: SELECT user_preferences ...
    DB-->>PrefRepo: preferences / none / error
    PrefRepo-->>InterestsC: Option<UserPreferences> / error
    InterestsC->>InterestsC: build_preferences_for_interest_update(...)
    InterestsC->>PrefRepo: save(updated_preferences, expected_revision?)
    PrefRepo->>DB: INSERT ... ON CONFLICT DO NOTHING / UPDATE ... WHERE revision = expected
    DB-->>PrefRepo: success / zero-rows / constraint violation
    PrefRepo-->>InterestsC: ok / RevisionMismatch(current_revision) / MissingForUpdate / ConcurrentWriteConflict / error
    InterestsC->>InterestsC: retry loop on retryable errors (up to MAX_CONCURRENT_WRITE_ATTEMPTS)
    InterestsC-->>State: UserInterests (on success) / Error
    State-->>Client: updated interests JSON / error response
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~75 minutes

Poem

Align Diesel seams and bind the pool,
Retry the write when races rule,
Fetch the profile, build the prefs anew,
Seed the tests, assert what must come true,
Merge adapters, let the pipelines spool.

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed The title directly and specifically describes the main change: implementing DB-backed user types with tests for roadmap item 3.5.3.
Description check ✅ Passed The description comprehensively outlines the PR's goals, changes, rationale, and implementation plan, all directly related to the changeset.
Docstring Coverage ✅ Passed Docstring coverage is 86.47% which is sufficient. The required threshold is 80.00%.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
  • 📝 Generate docstrings (stacked PR)
  • 📝 Generate docstrings (commit on current branch)
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch db-backed-user-profiles-3-5-3-lce3o6

Comment @coderabbitai help to get the list of available commands and usage tips.

Implement roadmap item 3.5.3 by replacing fixture-only profile and\ninterests wiring with explicit Diesel-backed adapters in DB-present\nstartup mode, while preserving fixture fallback with no DB pool.\n\nAdd dedicated outbound adapters for UserProfileQuery and\nUserInterestsCommand, wire them through state_builders, and export\nthem from the persistence module.\n\nExpand regression coverage with startup-mode integration and BDD suites\ncovering fixture fallback, DB-backed behaviour, schema-loss stability,\nand validation-envelope stability.\n\nUpdate architecture and roadmap docs to record the dedicated-adapter\ndecision and mark 3.5.3 complete.\n\nInclude formatting-only updates required by repository check-fmt gates.
codescene-delta-analysis[bot]

This comment was marked as outdated.

@leynos
Copy link
Copy Markdown
Owner Author

leynos commented Mar 5, 2026

@coderabbitai Please suggest a fix for this issue and supply a prompt for an AI coding agent to enable it to apply the fix:

backend/tests/user_state_profile_interests_startup_modes_bdd.rs

Comment on file

fn run_profile_interests_flow(world: &mut World) {
    if is_skipped(world) {
        return;
    }

    let db_pool = world.db.as_ref().expect("db context").pool.clone();
    let bind_addr = SocketAddr::from(([127, 0, 0, 1], 0));
    let config =
        ServerConfig::new(Key::generate(), false, SameSite::Lax, bind_addr).with_db_pool(db_pool);

    let state = state_builders::build_http_state(
        &config,
        Arc::new(FixtureRouteSubmissionService) as Arc<dyn RouteSubmissionService>,
    );
    let payload = InterestsRequest {
        interest_theme_ids: world.interests_payload.interest_theme_ids.clone(),
    };

    let (login_snapshot, profile_snapshot, interests_snapshot) = run_async(async move {
        let session = SessionMiddleware::builder(CookieSessionStore::default(), Key::generate())
            .cookie_name("session".to_owned())
            .cookie_path("/".to_owned())
            .cookie_secure(false)
            .cookie_http_only(true)
            .cookie_content_security(CookieContentSecurity::Private)
            .cookie_same_site(SameSite::Lax)
            .session_lifecycle(PersistentSession::default().session_ttl(CookieDuration::hours(2)))
            .build();

        let app = actix_test::init_service(
            App::new().app_data(state).wrap(backend::Trace).service(
                web::scope("/api/v1")
                    .wrap(session)
                    .service(login)
                    .service(current_user)
                    .service(update_interests),
            ),
        )
        .await;

        let login_req = actix_test::TestRequest::post()
            .uri("/api/v1/login")
            .set_json(&LoginRequest {
                username: "admin".to_owned(),
                password: "password".to_owned(),
            })
            .to_request();
        let login_res = actix_test::call_service(&app, login_req).await;
        let login_snapshot = Snapshot {
            status: login_res.status().as_u16(),
            trace_id: login_res
                .headers()
                .get(TRACE_ID_HEADER)
                .and_then(|value| value.to_str().ok())
                .map(ToOwned::to_owned),
            session_cookie: login_res
                .response()
                .cookies()
                .find(|cookie| cookie.name() == "session")
                .map(|cookie| cookie.into_owned()),
            body: parse_json_body(actix_test::read_body(login_res).await.as_ref()),
        };

        let Some(cookie) = login_snapshot.session_cookie.clone() else {
            return (login_snapshot, None, None);
        };

        let profile_req = actix_test::TestRequest::get()
            .uri("/api/v1/users/me")
            .cookie(cookie.clone())
            .to_request();
        let profile_res = actix_test::call_service(&app, profile_req).await;
        let profile_snapshot = Snapshot {
            status: profile_res.status().as_u16(),
            trace_id: profile_res
                .headers()
                .get(TRACE_ID_HEADER)
                .and_then(|value| value.to_str().ok())
                .map(ToOwned::to_owned),
            session_cookie: None,
            body: parse_json_body(actix_test::read_body(profile_res).await.as_ref()),
        };

        let interests_req = actix_test::TestRequest::put()
            .uri("/api/v1/users/me/interests")
            .cookie(cookie)
            .set_json(payload)
            .to_request();
        let interests_res = actix_test::call_service(&app, interests_req).await;
        let interests_snapshot = Snapshot {
            status: interests_res.status().as_u16(),
            trace_id: interests_res
                .headers()
                .get(TRACE_ID_HEADER)
                .and_then(|value| value.to_str().ok())
                .map(ToOwned::to_owned),
            session_cookie: None,
            body: parse_json_body(actix_test::read_body(interests_res).await.as_ref()),
        };

        (
            login_snapshot,
            Some(profile_snapshot),
            Some(interests_snapshot),
        )
    });

    world.login = Some(login_snapshot);
    world.profile = profile_snapshot;
    world.interests = interests_snapshot;
}

❌ New issue: Large Method
run_profile_interests_flow has 101 lines, threshold = 70

@leynos
Copy link
Copy Markdown
Owner Author

leynos commented Mar 5, 2026

@coderabbitai Please suggest a fix for this issue and supply a prompt for an AI coding agent to enable it to apply the fix:

backend/tests/diesel_profile_interests_adapters.rs

Comment on file

//! Focused startup-mode coverage for profile/interests routes.

❌ New issue: String Heavy Function Arguments
In this module, 53.3% of all arguments to its 10 functions are strings. The threshold for string arguments is 39.0%

@leynos
Copy link
Copy Markdown
Owner Author

leynos commented Mar 5, 2026

@coderabbitai Please suggest a fix for this issue and supply a prompt for an AI coding agent to enable it to apply the fix:

backend/tests/diesel_profile_interests_adapters.rs

Comment on file

async fn run_flow(
    state: web::Data<HttpState>,
    username: &str,
    password: &str,
    interests_payload: &InterestsRequest,
) -> (Snapshot, Option<Snapshot>, Option<Snapshot>) {
    let session = SessionMiddleware::builder(CookieSessionStore::default(), Key::generate())
        .cookie_name("session".to_owned())
        .cookie_path("/".to_owned())
        .cookie_secure(false)
        .cookie_http_only(true)
        .cookie_content_security(CookieContentSecurity::Private)
        .cookie_same_site(SameSite::Lax)
        .session_lifecycle(PersistentSession::default().session_ttl(CookieDuration::hours(2)))
        .build();

    let app = actix_test::init_service(
        App::new().app_data(state).wrap(backend::Trace).service(
            web::scope("/api/v1")
                .wrap(session)
                .service(login)
                .service(current_user)
                .service(update_interests),
        ),
    )
    .await;

    let login_req = actix_test::TestRequest::post()
        .uri("/api/v1/login")
        .set_json(&LoginRequest {
            username: username.to_owned(),
            password: password.to_owned(),
        })
        .to_request();
    let login_res = actix_test::call_service(&app, login_req).await;

    let login_snapshot = Snapshot {
        status: login_res.status().as_u16(),
        session_cookie: login_res
            .response()
            .cookies()
            .find(|cookie| cookie.name() == "session")
            .map(|cookie| cookie.into_owned()),
        body: parse_body(actix_test::read_body(login_res).await.as_ref()),
    };

    let Some(cookie) = login_snapshot.session_cookie.clone() else {
        return (login_snapshot, None, None);
    };

    let profile_req = actix_test::TestRequest::get()
        .uri("/api/v1/users/me")
        .cookie(cookie.clone())
        .to_request();
    let profile_res = actix_test::call_service(&app, profile_req).await;
    let profile_snapshot = Snapshot {
        status: profile_res.status().as_u16(),
        session_cookie: None,
        body: parse_body(actix_test::read_body(profile_res).await.as_ref()),
    };

    let interests_req = actix_test::TestRequest::put()
        .uri("/api/v1/users/me/interests")
        .cookie(cookie)
        .set_json(interests_payload)
        .to_request();
    let interests_res = actix_test::call_service(&app, interests_req).await;
    let interests_snapshot = Snapshot {
        status: interests_res.status().as_u16(),
        session_cookie: None,
        body: parse_body(actix_test::read_body(interests_res).await.as_ref()),
    };

    (
        login_snapshot,
        Some(profile_snapshot),
        Some(interests_snapshot),
    )
}

❌ New issue: Large Method
run_flow has 72 lines, threshold = 70

@coderabbitai

This comment was marked as resolved.

@coderabbitai

This comment was marked as resolved.

@coderabbitai

This comment was marked as resolved.

Reduce string-heavy function argument usage in\nbackend/tests/diesel_profile_interests_adapters.rs by introducing\nstronger local types and shared credentials input.\n\n- Add Credentials struct and use it in run_flow\n- Type seed_user and db_contains_interest_id UUID parameters as Uuid\n- Update call sites without changing test assertions or constants
codescene-delta-analysis[bot]

This comment was marked as outdated.

Extract session middleware, response snapshot capture, and profile/interests\nflow execution helpers in user_state_profile_interests_startup_modes_bdd\nto reduce run_profile_interests_flow complexity while preserving behaviour.\n\nKeep the cookie guard semantics and world assignment flow unchanged.
codescene-delta-analysis[bot]

This comment was marked as outdated.

Extract helper functions from run_flow to reduce method size and remove\nstring-heavy argument passing.\n\nChanges include:\n- add build_test_app, do_login, do_get_profile, and\n  do_update_interests helpers\n- update run_flow to orchestrate those helpers with\n  Credentials input\n- keep existing test assertions and behaviour unchanged\n\nThis keeps startup-mode test coverage intact while addressing\nCodeScene large-method and argument-smell findings.
codescene-delta-analysis[bot]

This comment was marked as outdated.

@leynos
Copy link
Copy Markdown
Owner Author

leynos commented Mar 5, 2026

@coderabbitai Please suggest a fix for this issue and supply a prompt for an AI coding agent to enable it to apply the fix:

backend/tests/diesel_profile_interests_adapters.rs

Comment on file

async fn do_get_profile<S>(app: &S, cookie: Cookie<'static>) -> Snapshot
where
    S: actix_web::dev::Service<
            actix_http::Request,
            Response = actix_web::dev::ServiceResponse<actix_web::body::BoxBody>,
            Error = actix_web::Error,
        >,
{
    let profile_req = actix_test::TestRequest::get()
        .uri("/api/v1/users/me")
        .cookie(cookie)
        .to_request();
    let profile_res = actix_test::call_service(app, profile_req).await;

    Snapshot {
        status: profile_res.status().as_u16(),
        session_cookie: None,
        body: parse_body(actix_test::read_body(profile_res).await.as_ref()),
    }
}

❌ New issue: Code Duplication
The module contains 2 functions with similar structure: do_get_profile,do_update_interests

@coderabbitai

This comment was marked as resolved.

Extract a shared call_and_capture helper for the profile and\ninterests request helpers in the Diesel adapter tests.\n\nThis removes the remaining duplicated Actix request execution and\nSnapshot construction while preserving the existing test flow and\nassertions.
codescene-delta-analysis[bot]

This comment was marked as outdated.

Override the transitive DOMPurify dependency used by Mermaid tooling\nso workspace audits no longer fail on the current XSS advisories.\n\nThis updates the root pnpm override and refreshes the lockfile to\nresolve DOMPurify 3.3.2 across the affected dependency paths.
codescene-delta-analysis[bot]

This comment was marked as outdated.

@leynos
Copy link
Copy Markdown
Owner Author

leynos commented Mar 6, 2026

@coderabbitai Please suggest a fix for this issue and supply a prompt for an AI coding agent to enable it to apply the fix:

Code Duplication

backend/tests/diesel_profile_interests_adapters.rs:

What lead to degradation?

The module contains 2 functions with similar structure: do_get_profile,do_update_interests

Why does this problem occur?

Duplicated code often leads to code that's harder to change since the same logical change has to be done in multiple functions. More duplication gives lower code health.

How to fix it?

A certain degree of duplicated code might be acceptable. The problems start when it is the same behavior that is duplicated across the functions in the module, ie. a violation of the Don't Repeat Yourself (DRY) principle. DRY violations lead to code that is changed together in predictable patterns, which is both expensive and risky. DRY violations can be identified using CodeScene's X-Ray analysis to detect clusters of change coupled functions with high code similarity. Read More
Once you have identified the similarities across functions, look to extract and encapsulate the concept that varies into its own function(s). These shared abstractions can then be re-used, which minimizes the amount of duplication and simplifies change.

@coderabbitai

This comment was marked as resolved.

@leynos
Copy link
Copy Markdown
Owner Author

leynos commented Mar 6, 2026

@coderabbitai Please suggest a fix for this issue and supply a prompt for an AI coding agent to enable it to apply the fix:

Code Duplication

backend/tests/diesel_profile_interests_adapters.rs:

What lead to degradation?

The module contains 2 functions with similar structure: do_get_profile,do_update_interests

Why does this problem occur?

Duplicated code often leads to code that's harder to change since the same logical change has to be done in multiple functions. More duplication gives lower code health.

How to fix it?

A certain degree of duplicated code might be acceptable. The problems start when it is the same behavior that is duplicated across the functions in the module, ie. a violation of the Don't Repeat Yourself (DRY) principle. DRY violations lead to code that is changed together in predictable patterns, which is both expensive and risky. DRY violations can be identified using CodeScene's X-Ray analysis to detect clusters of change coupled functions with high code similarity. Read More
Once you have identified the similarities across functions, look to extract and encapsulate the concept that varies into its own function(s). These shared abstractions can then be re-used, which minimizes the amount of duplication and simplifies change.

@coderabbitai

This comment was marked as resolved.

@leynos
Copy link
Copy Markdown
Owner Author

leynos commented Mar 6, 2026

@CodeRabbit if you feel that no further action is needed with respect to this comment, please provide a suitable message to accompany the lint suppression to be applied to the codescene diagnostic.

Copy link
Copy Markdown
Contributor

@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: 5

Caution

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

⚠️ Outside diff range comments (1)
backend/src/outbound/persistence/diesel_user_preferences_repository.rs (1)

167-173: ⚠️ Potential issue | 🟡 Minor

Validate revision bounds at the write boundary to match read-side behaviour.

cast_revision_for_db currently uses an unchecked cast (revision as i32), whilst the symmetric cast_revision_from_db function correctly validates with i32::try_from. This asymmetry allows oversized revisions to wrap silently instead of surfacing a repository-level error. Refactor cast_revision_for_db to return Result<i32, UserPreferencesRepositoryError> and propagate errors at lines 207 and 240 using the ? operator (the save function already returns Result).

Proposed fix
-#[expect(
-    clippy::cast_possible_wrap,
-    reason = "revision values are always small positive integers"
-)]
-fn cast_revision_for_db(revision: u32) -> i32 {
-    revision as i32
+fn cast_revision_for_db(revision: u32) -> Result<i32, UserPreferencesRepositoryError> {
+    i32::try_from(revision).map_err(|_| {
+        UserPreferencesRepositoryError::query(format!("revision out of range: {revision}"))
+    })
 }
-        let revision_i32 = cast_revision_for_db(preferences.revision);
+        let revision_i32 = cast_revision_for_db(preferences.revision)?;
-                let expected_i32 = cast_revision_for_db(expected);
+                let expected_i32 = cast_revision_for_db(expected)?;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@backend/src/outbound/persistence/diesel_user_preferences_repository.rs`
around lines 167 - 173, Replace the unchecked cast in cast_revision_for_db with
a fallible conversion: change cast_revision_for_db to return Result<i32,
UserPreferencesRepositoryError> and use i32::try_from(revision).map_err(|_|
UserPreferencesRepositoryError::query(format!("revision out of range:
{revision}"))) so oversized u32 revisions produce an error instead of wrapping;
then update callers in save (where revision_i32 and expected_i32 are computed)
to propagate the error with the ? operator (i.e., let revision_i32 =
cast_revision_for_db(preferences.revision)? and let expected_i32 =
cast_revision_for_db(expected)?), keeping the save function's existing Result
return type.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@backend/src/domain/preferences_service_tests.rs`:
- Around line 100-167: Two nearly identical tests
update_maps_missing_for_update_to_conflict and
update_maps_concurrent_write_conflict_to_conflict should be merged into a single
parameterized rstest: create an #[rstest] with parameters for the mocked
repository behavior (the error returned by
MockUserPreferencesRepository::expect_save) and the expected JSON detail check,
perform the shared setup once (create user_id, repo,
repo.expect_find_by_user_id, make_service, and build UpdatePreferencesRequest),
then in the body set repo.expect_save().return_once to return the injected error
and assert the single error path (error.code() == ErrorCode::Conflict and the
details match the provided expected value) instead of duplicating the full
service/test setup across two tests; reference test names
update_maps_missing_for_update_to_conflict,
update_maps_concurrent_write_conflict_to_conflict,
MockUserPreferencesRepository,
UserPreferencesRepositoryError::missing_for_update,
UserPreferencesRepositoryError::concurrent_write_conflict, make_service, and
UpdatePreferencesRequest to locate and refactor the code.

In `@backend/src/outbound/persistence/diesel_user_interests_command.rs`:
- Around line 77-83: Change the mapping for
UserPreferencesRepositoryError::RevisionMismatch to return an Error::conflict
rather than Error::internal and attach a details payload containing the expected
and actual revisions; specifically, in the match arm for
UserPreferencesRepositoryError::RevisionMismatch { expected, actual } replace
the internal error with Error::conflict("preferences changed
concurrently").with_details(json!({ "code": "revision_mismatch",
"expectedRevision": expected, "actualRevision": actual })) so callers receive a
409-style semantic error and can inspect the revision fields.

In
`@backend/src/outbound/persistence/diesel_user_interests_command/tests/retry.rs`:
- Around line 114-119: Add assertions that the persisted interest IDs match
expected values in the retry tests by checking
saved_preferences.interest_theme_ids from the repository.last_save_call()
result; after retrieving (saved_preferences, expected_revision) (the tuple
returned by repository.last_save_call() used in these tests), assert that
saved_preferences.interest_theme_ids equals the expected vector/collection for
the retry case, and add the same check to the other test that uses
repository.last_save_call() around the second retry (the one mirrored by the
block at the later test), ensuring both tests validate the persisted IDs as well
as the DTO and revision.
- Around line 8-145: Collapse the four separate tokio tests into a single
parameterised rstest that iterates over the four retry scenarios and uses a
small helper assertion to check the saved result; specifically, replace the
individual tests set_interests_retries_after_insert_race_revision_mismatch,
set_interests_retries_after_stale_update_revision_mismatch,
set_interests_retries_after_missing_preferences_for_update, and
set_interests_retries_after_concurrent_write_conflict with an #[rstest] that
supplies (repository, expected_revision, expected_saved_revision,
expected_interest_uuid) cases, reuse DieselUserInterestsCommand::new(...) and
its set_interests(&user_id, ...) call in a shared fixture, and assert via a
helper that calls repository.last_save_call() and compares expected_revision and
saved_preferences.revision; ensure you still configure repositories the same way
(InsertRaceRetryRepository, StaleUpdateRetryRepository,
StubUserPreferencesRepository + set_save_failure(StubFailure::...), and
interest_theme_id values) so the test logic and expectations remain identical.

In
`@backend/src/outbound/persistence/diesel_user_interests_command/tests/support.rs`:
- Around line 188-213: The stub save method in StubUserPreferencesRepository
currently overwrites stored_preferences unconditionally; change save to enforce
expected_revision semantics by first reading the current stored_preferences
(from self.stored_preferences) and rejecting the call when expected_revision is
None but no existing row (return a not-found/appropriate
UserPreferencesRepositoryError) or when expected_revision is Some(rev) that does
not match the current revision (return a revision-mismatch error) instead of
writing; preserve the existing save_failures handling and only update
self.stored_preferences and self.last_save after the revision checks pass.

---

Outside diff comments:
In `@backend/src/outbound/persistence/diesel_user_preferences_repository.rs`:
- Around line 167-173: Replace the unchecked cast in cast_revision_for_db with a
fallible conversion: change cast_revision_for_db to return Result<i32,
UserPreferencesRepositoryError> and use i32::try_from(revision).map_err(|_|
UserPreferencesRepositoryError::query(format!("revision out of range:
{revision}"))) so oversized u32 revisions produce an error instead of wrapping;
then update callers in save (where revision_i32 and expected_i32 are computed)
to propagate the error with the ? operator (i.e., let revision_i32 =
cast_revision_for_db(preferences.revision)? and let expected_i32 =
cast_revision_for_db(expected)?), keeping the save function's existing Result
return type.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 96d78c8b-f7e3-457b-8b4e-2884a0db5453

📥 Commits

Reviewing files that changed from the base of the PR and between 0915c6b and 44b0bd2.

📒 Files selected for processing (11)
  • backend/migrations/2026-03-07-000000_add_non_negative_revision_checks/down.sql
  • backend/migrations/2026-03-07-000000_add_non_negative_revision_checks/up.sql
  • backend/src/domain/ports/user_preferences_repository.rs
  • backend/src/domain/preferences_service.rs
  • backend/src/domain/preferences_service_tests.rs
  • backend/src/outbound/persistence/diesel_user_interests_command.rs
  • backend/src/outbound/persistence/diesel_user_interests_command/tests/retry.rs
  • backend/src/outbound/persistence/diesel_user_interests_command/tests/support.rs
  • backend/src/outbound/persistence/diesel_user_preferences_repository.rs
  • backend/tests/diesel_route_annotation_repository.rs
  • backend/tests/diesel_user_preferences_repository.rs

Comment thread backend/src/domain/preferences_service_tests.rs Outdated
Comment thread backend/src/outbound/persistence/diesel_user_interests_command.rs
Comment thread backend/src/outbound/persistence/diesel_user_interests_command/tests/retry.rs Outdated
Comment thread backend/src/outbound/persistence/diesel_user_interests_command/tests/retry.rs Outdated
codescene-delta-analysis[bot]

This comment was marked as outdated.

leynos added 2 commits March 7, 2026 19:28
Extract shared RetrySaveTracker helpers for the retry repositories\nso the duplicated find and save test paths collapse into one\nplace.\n\nKeep the scenario-specific mismatch tuples and messages intact,\nand use a tightly scoped clippy expectation because the\nCodeScene-directed helper shape exceeds the repo's argument\nthreshold.\n\nCo-authored-by: Codex <noreply@openai.com>
Verify the reported findings against the current branch and\napply only the live fixes.\n\n- map revision mismatches in the interests command to conflict\n  errors with structured details\n- make the Diesel revision-to-DB conversion fallible and cover it\n  with a regression test\n- enforce optimistic-concurrency semantics in the stub\n  preferences repository used by retry coverage\n- collapse the duplicated preferences-service and retry tests into\n  parameterised rstest cases while preserving their assertions\n\nCo-authored-by: Codex <noreply@openai.com>
codescene-delta-analysis[bot]

This comment was marked as outdated.

codescene-delta-analysis[bot]

This comment was marked as outdated.

Copy link
Copy Markdown
Contributor

@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: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In
`@backend/src/outbound/persistence/diesel_user_interests_command/tests/retry.rs`:
- Around line 50-130: The function build_retry_case is flagged by CodeScene for
exceeding the line threshold but is intentionally cohesive; add a short
justification comment directly above build_retry_case explaining that the
function intentionally keeps all RetryScenario arms together for readability and
test fixture integrity (mentioning RetryScenario, RetryCase, and the specific
repository builders InsertRaceRetryRepository, StaleUpdateRetryRepository, and
StubUserPreferencesRepository), so automated tooling reviewers know this is
deliberate and the function should not be split.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 617930f9-26a6-461a-aff4-22605b1ad368

📥 Commits

Reviewing files that changed from the base of the PR and between 44b0bd2 and b85db84.

📒 Files selected for processing (6)
  • backend/src/domain/preferences_service_tests.rs
  • backend/src/outbound/persistence/diesel_user_interests_command.rs
  • backend/src/outbound/persistence/diesel_user_interests_command/tests/mapping.rs
  • backend/src/outbound/persistence/diesel_user_interests_command/tests/retry.rs
  • backend/src/outbound/persistence/diesel_user_interests_command/tests/support.rs
  • backend/src/outbound/persistence/diesel_user_preferences_repository.rs

Comment thread backend/src/outbound/persistence/diesel_user_interests_command/tests/retry.rs Outdated
@leynos
Copy link
Copy Markdown
Owner Author

leynos commented Mar 7, 2026

@coderabbitai Please suggest a fix for this issue and supply a prompt for an AI coding agent to enable it to apply the fix:

backend/src/domain/preferences_service_tests.rs

Comment on file

    MutationType, UnitSystem, UserId, UserPreferences,
};
use chrono::Utc;
use rstest::rstest;

❌ New issue: Code Duplication
The module contains 2 functions with similar structure: update_rejects_missing_revision_when_record_exists,update_rejects_revision_mismatch

@leynos
Copy link
Copy Markdown
Owner Author

leynos commented Mar 7, 2026

@coderabbitai Please suggest a fix for this issue and supply a prompt for an AI coding agent to enable it to apply the fix:

backend/src/outbound/persistence/diesel_user_interests_command/tests/support.rs

Comment on file

    async fn find_by_user_id(
        &self,
        user_id: &UserId,
    ) -> Result<Option<UserPreferences>, UserPreferencesRepositoryError> {
        if *user_id != self.user_id {
            return Ok(None);
        }

        Ok(self
            .tracker
            .select_preferences(None, &self.competing_preferences)
            .cloned())
    }

❌ New issue: Code Duplication
The module contains 4 functions with similar structure: UserPreferencesRepository.find_by_user_id,UserPreferencesRepository.find_by_user_id,UserPreferencesRepository.save,UserPreferencesRepository.save

@leynos
Copy link
Copy Markdown
Owner Author

leynos commented Mar 7, 2026

@coderabbitai Please suggest a fix for this issue and supply a prompt for an AI coding agent to enable it to apply the fix:

backend/src/outbound/persistence/diesel_user_interests_command/tests/retry.rs

Comment on file

fn build_retry_case(scenario: RetryScenario) -> RetryCase {
    let user_id = user_id();

    match scenario {
        RetryScenario::InsertRaceRevisionMismatch => RetryCase {
            repository: RetryRepositoryHarness::InsertRace(Arc::new(
                InsertRaceRetryRepository::new(
                    user_id.clone(),
                    UserPreferences::builder(user_id.clone())
                        .interest_theme_ids(vec![uuid_id("3fa85f64-5717-4562-b3fc-2c963f66afa9")])
                        .safety_toggle_ids(Vec::new())
                        .unit_system(UnitSystem::Metric)
                        .revision(1)
                        .build(),
                ),
            )),
            user_id,
            interest_theme_ids: vec![interest_theme_id("3fa85f64-5717-4562-b3fc-2c963f66afa6")],
            expected_revision: Some(1),
            expected_saved_revision: 2,
            expected_saved_interest_ids: vec![uuid_id("3fa85f64-5717-4562-b3fc-2c963f66afa6")],
        },
        RetryScenario::StaleUpdateRevisionMismatch => RetryCase {
            repository: RetryRepositoryHarness::StaleUpdate(Arc::new(
                StaleUpdateRetryRepository::new(
                    UserPreferences::builder(user_id.clone())
                        .interest_theme_ids(vec![uuid_id("3fa85f64-5717-4562-b3fc-2c963f66afa6")])
                        .safety_toggle_ids(vec![uuid_id("3fa85f64-5717-4562-b3fc-2c963f66afa8")])
                        .unit_system(UnitSystem::Metric)
                        .revision(2)
                        .build(),
                    UserPreferences::builder(user_id.clone())
                        .interest_theme_ids(vec![uuid_id("3fa85f64-5717-4562-b3fc-2c963f66afaa")])
                        .safety_toggle_ids(vec![uuid_id("3fa85f64-5717-4562-b3fc-2c963f66afa8")])
                        .unit_system(UnitSystem::Metric)
                        .revision(3)
                        .build(),
                ),
            )),
            user_id,
            interest_theme_ids: vec![interest_theme_id("3fa85f64-5717-4562-b3fc-2c963f66afa7")],
            expected_revision: Some(3),
            expected_saved_revision: 4,
            expected_saved_interest_ids: vec![uuid_id("3fa85f64-5717-4562-b3fc-2c963f66afa7")],
        },
        RetryScenario::MissingPreferencesForUpdate => {
            let existing_preferences = UserPreferences::builder(user_id.clone())
                .interest_theme_ids(vec![uuid_id("3fa85f64-5717-4562-b3fc-2c963f66afa6")])
                .safety_toggle_ids(vec![uuid_id("3fa85f64-5717-4562-b3fc-2c963f66afa8")])
                .unit_system(UnitSystem::Metric)
                .revision(4)
                .build();
            let repository = Arc::new(StubUserPreferencesRepository::with_preferences(
                existing_preferences,
            ));
            repository.set_save_failure(StubFailure::MissingForUpdate { expected: 4 });

            RetryCase {
                repository: RetryRepositoryHarness::Stub(repository),
                user_id,
                interest_theme_ids: vec![interest_theme_id("3fa85f64-5717-4562-b3fc-2c963f66afa7")],
                expected_revision: Some(4),
                expected_saved_revision: 5,
                expected_saved_interest_ids: vec![uuid_id("3fa85f64-5717-4562-b3fc-2c963f66afa7")],
            }
        }
        RetryScenario::ConcurrentWriteConflict => {
            let repository = Arc::new(StubUserPreferencesRepository::default());
            repository.set_save_failure(StubFailure::ConcurrentWriteConflict);

            RetryCase {
                repository: RetryRepositoryHarness::Stub(repository),
                user_id,
                interest_theme_ids: vec![interest_theme_id("3fa85f64-5717-4562-b3fc-2c963f66afa6")],
                expected_revision: None,
                expected_saved_revision: 1,
                expected_saved_interest_ids: vec![uuid_id("3fa85f64-5717-4562-b3fc-2c963f66afa6")],
            }
        }
    }
}

❌ New issue: Large Method
build_retry_case has 78 lines, threshold = 70

@coderabbitai

This comment was marked as resolved.

@coderabbitai

This comment was marked as resolved.

@leynos
Copy link
Copy Markdown
Owner Author

leynos commented Mar 7, 2026

@coderabbitai Have the following now been resolved?

Annul any requirements that violate the en-GB-oxendict spelling (-ize / -yse / -our) conventions (for example a request to replace "normalize" with "normalise" or "artefact" with "artifact"), or where the requirement unnecessarily increases cyclomatic complexity.

Please address the following concerns and ensure all commit gates succeed:

⚠️ Outside diff range comments (1)

backend/src/outbound/persistence/diesel_user_preferences_repository.rs (1)

167-173: ⚠️ Potential issue | 🟡 Minor
Validate revision bounds at the write boundary to match read-side behaviour.
cast_revision_for_db currently uses an unchecked cast (revision as i32), whilst the symmetric cast_revision_from_db function correctly validates with i32::try_from. This asymmetry allows oversized revisions to wrap silently instead of surfacing a repository-level error. Refactor cast_revision_for_db to return Result<i32, UserPreferencesRepositoryError> and propagate errors at lines 207 and 240 using the ? operator (the save function already returns Result).

Proposed fix
-#[expect(
-    clippy::cast_possible_wrap,
-    reason = "revision values are always small positive integers"
-)]
-fn cast_revision_for_db(revision: u32) -> i32 {
-    revision as i32
+fn cast_revision_for_db(revision: u32) -> Result<i32, UserPreferencesRepositoryError> {
+    i32::try_from(revision).map_err(|_| {
+        UserPreferencesRepositoryError::query(format!("revision out of range: {revision}"))
+    })
 }

@coderabbitai

This comment was marked as resolved.

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Mar 7, 2026

Tip

For best results, initiate chat on the files or code changes.

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
fd 'diesel_user_preferences_repository.rs' --exec cat {}

Length of output: 19791


@leynos: This is already fully resolved on the branch. cast_revision_for_db now returns Result<i32, UserPreferencesRepositoryError>, using i32::try_from with a mapped error — identical to the proposed fix:

fn cast_revision_for_db(revision: u32) -> Result<i32, UserPreferencesRepositoryError> {
    i32::try_from(revision).map_err(|_| {
        UserPreferencesRepositoryError::query(format!("revision out of range: {revision}"))
    })
}

Both call sites in save propagate the error with ?, and there is a unit test — cast_revision_for_db_rejects_oversized_values — that asserts u32::MAX produces a Query error containing "revision out of range". No further changes are needed.

codescene-delta-analysis[bot]

This comment was marked as outdated.

Extract the four retry-case constructors out of build_retry_case\nso the dispatcher stays below the method-size threshold without\nchanging any scenario data or test behaviour.\n\nCo-authored-by: Codex <noreply@openai.com>
codescene-delta-analysis[bot]

This comment was marked as outdated.

Copy link
Copy Markdown
Contributor

@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: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In
`@backend/src/outbound/persistence/diesel_user_interests_command/tests/retry.rs`:
- Around line 146-149: Rename the confusing variable actual_expected_revision to
a clearer name (e.g., passed_expected_revision or recorded_expected_revision) in
the test where you destructure repository.last_save_call(); update the let
binding (let (saved_preferences, passed_expected_revision) =
repository.last_save_call().expect(...)) and any subsequent uses (the assert_eq!
comparing it to expected_revision) so the name reflects that this is the
expected-revision value passed into the save call.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: a3775cdf-4c15-4ffb-a59e-43c90fd9fbb4

📥 Commits

Reviewing files that changed from the base of the PR and between b85db84 and 695b92b.

📒 Files selected for processing (1)
  • backend/src/outbound/persistence/diesel_user_interests_command/tests/retry.rs

Comment thread backend/src/outbound/persistence/diesel_user_interests_command/tests/retry.rs Outdated
Replace two duplicated preferences-service conflict tests with one\nparameterised rstest while preserving the original missing\nrevision and mismatched revision scenarios.\n\nThis keeps the conflict-path assertions identical and reduces\nduplication in the test module.\n\nCo-authored-by: Codex <noreply@openai.com>
Rename the saved-call expected revision binding in the retry\nassertion helper so it reflects the value recorded on the\nrepository save call.\n\nThis keeps the test logic unchanged while making the assertion\nintent easier to read.\n\nCo-authored-by: Codex <noreply@openai.com>
@leynos
Copy link
Copy Markdown
Owner Author

leynos commented Mar 7, 2026

@coderabbitai review

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Mar 7, 2026

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@leynos
Copy link
Copy Markdown
Owner Author

leynos commented Mar 7, 2026

@coderabbitai review

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Mar 7, 2026

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@leynos leynos merged commit faef699 into main Mar 7, 2026
4 checks passed
@leynos leynos deleted the db-backed-user-profiles-3-5-3-lce3o6 branch March 7, 2026 23:26
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.

1 participant