Skip to content
This repository was archived by the owner on Sep 30, 2025. It is now read-only.

Comments

feat: finish chapter 7#12

Merged
TN19N merged 1 commit intomainfrom
10-chapter-7
Sep 1, 2025
Merged

feat: finish chapter 7#12
TN19N merged 1 commit intomainfrom
10-chapter-7

Conversation

@TN19N
Copy link
Owner

@TN19N TN19N commented Sep 1, 2025

Summary by CodeRabbit

  • New Features
    • Added email-based subscription confirmation: users receive a confirmation link; confirming activates their subscription (PENDING → CONFIRMED).
    • Exposed confirmation endpoint at /subscriptions/confirm.
  • Configuration
    • Introduced new environment variables for email sending and service/base URLs; updated sample/test env files and deployment config.
  • Performance
    • Optimized release builds for smaller, faster binaries.
  • Tests
    • Added end-to-end tests covering health checks, subscription flow, email delivery, and confirmation.

@TN19N TN19N self-assigned this Sep 1, 2025
@TN19N TN19N added the enhancement New feature or request label Sep 1, 2025
@TN19N TN19N linked an issue Sep 1, 2025 that may be closed by this pull request
@coderabbitai
Copy link
Contributor

coderabbitai bot commented Sep 1, 2025

Walkthrough

Introduces email client support and confirmation token flow for subscriptions; restructures handlers; extends configuration (base URLs, email client settings); updates database schema (subscription_tokens table, status/token fields); adjusts startup/state ownership and wiring; adds release profile and dependencies; adds migrations task; updates CI env; adds comprehensive integration tests; replaces/renames config fields.

Changes

Cohort / File(s) Summary
Environment configs
/.env.example, /.env.test
Add SUBSCRIPTIONS__BASE_URL; replace SUBSCRIPTIONS__DATABASE__URL with SUBSCRIPTIONS__DATABASE__BASE_URL; introduce email client vars (sender, base URL, auth token, timeout); set test RUST_LOG=debug; comments and trailing newline.
CI/CD workflow
.github/workflows/cd.yml
Update Cloud Run env vars: add BASE_URL, DATABASE__BASE_URL; add email client vars; remove DATABASE__URL.
Task runner
.moon/tasks/rust-backend-application.yml
Add local migrations task running surrealdb-migrations with .env.
Build/deps
/Cargo.toml
Add optimized release profile; add reqwest(https/json), serde-humantime, serde_json, rand(std_rng); bump config/tracing-subscriber; move claims to dev; add wiremock, axum-test, linkify; remove http-body-util.
DB schema & migrations
/migrations/definitions/_initial.json, /schemas/subscription_tokens.surql, /schemas/subscriptions.surql
Add subscription_tokens table + unique index; add subscriptions.status (PENDING
Config & domain types
/src/config.rs, /src/domain/mod.rs, /src/domain/subscriber/mod.rs, /src/domain/subscriber/email.rs
Add EmailClientConfig; add Config.base_url and email_client; rename DatabaseConfig.url→base_url; make SubscriberEmail Clone + Deserialize; re-export SubscriberEmail.
Email client
/src/email_client.rs, /src/lib.rs
Add EmailClient with constructor and send_email using reqwest, headers, JSON payload; wire module.
Error handling
/src/errors.rs
Extend Error enum: separate ValidationErrors and ValidationError; add Reqwest and Custom; map both validation variants to 400.
Handlers refactor
/src/handlers.rs (removed), /src/handlers/mod.rs, /src/handlers/health_check.rs, /src/handlers/subscription.rs
Split handlers into modules; re-add health; add subscription flow with subscribe and confirm endpoints, token generation, and email sending.
Model changes
/src/model.rs
Change create_subscriber signature (by-ref + token); implement transactional creation of token + subscription; use config.base_url; adjust mem detection.
Startup & state wiring
/src/startup.rs, /src/state.rs, /src/main.rs
init takes Config by value; add GET /subscriptions/confirm; AppState stores Arc and Arc; FromRef impls; construct EmailClient from config; call site updated.
Tests
/tests/api/..., /tests/tests.rs
Add integration tests for health, subscriptions, confirmation, and helpers (mock email server, link extraction); remove old monolithic tests file.

Sequence Diagram(s)

sequenceDiagram
  autonumber
  actor User
  participant Router
  participant SubscriptionHandler as Subscription Handler
  participant Model as ModelManager/DB
  participant EmailClient
  participant MailService as Email Service

  Note over User,Router: Subscribe flow
  User->>Router: POST /subscriptions (name, email)
  Router->>SubscriptionHandler: subscribe(FormData)
  SubscriptionHandler->>SubscriptionHandler: generate token
  SubscriptionHandler->>Model: create_subscriber(&Subscriber, token)
  Model->>Model: begin transaction
  Model->>Model: INSERT subscription_tokens(token)
  Model->>Model: INSERT subscriptions(email,name,token_ref,status=PENDING)
  Model-->>SubscriptionHandler: ok
  SubscriptionHandler->>EmailClient: send_email(to, subject, html/text with confirm link)
  EmailClient->>MailService: POST / (JSON, auth header)
  MailService-->>EmailClient: 200 OK
  EmailClient-->>SubscriptionHandler: ok
  SubscriptionHandler-->>Router: 201 Created
  Router-->>User: 201

  Note over User,Router: Confirm flow
  User->>Router: GET /subscriptions/confirm?token=...
  Router->>SubscriptionHandler: confirm(Query{token})
  SubscriptionHandler->>Model: UPDATE subscriptions SET status=CONFIRMED WHERE token matches AND status=PENDING
  Model-->>SubscriptionHandler: ok
  SubscriptionHandler-->>Router: 200 OK
  Router-->>User: 200
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

  • feat/chapter 6 #9 — Overlaps on subscription domain, handlers, model, and validator-related error variants; likely preceding work enabling current refactors.

Poem

In burrows of code I hop with delight,
Tokens and letters now taking flight.
A click, a confirm, a shining green light—
Emails dispatched through tunnels of byte.
Thump goes the queue, all statuses right. 🐇✉️

✨ Finishing Touches
  • 📝 Generate Docstrings
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch 10-chapter-7

🪧 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.

Copy link
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: 10

Caution

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

⚠️ Outside diff range comments (2)
.github/workflows/cd.yml (2)

23-25: Fix missing step ID for Buildx output reference

You reference steps.setup-buildx.outputs.name but the Buildx step has no id, so the variable will be empty at runtime.

Apply:

-      - name: Set up Docker Buildx
+      - name: Set up Docker Buildx
+        id: setup-buildx
         uses: docker/setup-buildx-action@v3

Also applies to: 35-37


63-66: Set an ID on the Cloud Run deploy step

steps.deploy.outputs.url is used later, but the deploy step lacks id: deploy.

Apply:

-      - name: Deploy to Google Cloud Run
+      - name: Deploy to Google Cloud Run
+        id: deploy
         uses: google-github-actions/deploy-cloudrun@v2

Also applies to: 84-85

🧹 Nitpick comments (33)
.github/workflows/cd.yml (1)

9-19: Avoid overlapping deployments

Optional: add a concurrency group to serialize deploys on main.

Apply:

 jobs:
   deploy:
     runs-on: ubuntu-latest
     environment: production
+    concurrency:
+      group: cloudrun-${{ github.ref_name }}
+      cancel-in-progress: false
src/domain/subscriber/email.rs (2)

14-24: Add TryFrom<&str> to avoid temporary allocations

Handy for call sites holding &str.

Apply:

 impl TryFrom<String> for SubscriberEmail {
@@
 }
+
+impl TryFrom<&str> for SubscriberEmail {
+    type Error = validator::ValidationError;
+    fn try_from(value: &str) -> Result<Self, Self::Error> {
+        Self::try_from(value.to_owned())
+    }
+}

59-64: Add serde-path tests

Add tests asserting that deserializing an invalid email into SubscriberEmail fails (ensures the above serde wiring stays enforced).

Example (new test):

#[test]
fn serde_rejects_invalid_email() {
    #[derive(Deserialize)]
    struct T { email: SubscriberEmail }
    let json = r#"{ "email": "@domain.com" }"#;
    let err = serde_json::from_str::<T>(json).unwrap_err();
    assert!(err.is_data());
}
.moon/tasks/rust-backend-application.yml (1)

8-13: Ensure surrealdb-migrations CLI is available in PATH

This task will fail if the CLI isn’t installed on CI/dev machines. Either install it in CI or invoke it via cargo if it’s in your workspace.

Option A (install in CI before running): cargo install surrealdb-migrations-cli (or your chosen distribution).
Option B (invoke via cargo if available as a bin in the workspace):

-  migrations:
-    command: surrealdb-migrations
+  migrations:
+    command: cargo run -q --bin surrealdb-migrations --
     description: An awesome CLI for SurrealDB migrations (provides commands to scaffold, create and apply migrations)
     local: true
     options:
       envFile: .env
schemas/subscription_tokens.surql (2)

10-10: Use consistent ON TABLE syntax for index (minor)

For consistency with fields, prefer ON TABLE.

Apply:

-DEFINE INDEX OVERWRITE unique_token ON subscription_tokens COLUMNS token UNIQUE;
+DEFINE INDEX OVERWRITE unique_token ON TABLE subscription_tokens COLUMNS token UNIQUE;

6-7: Optional: plan for token TTL/cleanup

If tokens should expire, consider an index to support pruning and a scheduled cleanup (or time-based assertions).

src/errors.rs (2)

18-25: Good variant additions; prefer Display over Debug for String to avoid quoted logs

Using {0:?} on Custom(String) prints quotes/escapes in logs. Switch to {0} for cleaner, consistent formatting.

-    #[error("{0:?}")]
-    Custom(String),
+    #[error("{0}")]
+    Custom(String),

31-38: Return a helpful body for 400 validation errors (matches your earlier preference to surface the offending email); also tidy log text

Right now 400s have an empty body. If you want clients/tests to see the validation message (incl. the email value), return it as the response body. Also fix the log phrasing.

-            Self::ValidationErrors(_) | Self::ValidationError(_) => {
-                tracing::info!("Bad request: - {self:?}");
-                StatusCode::BAD_REQUEST.into_response()
-            }
-            _ => {
-                tracing::error!("Internal Server: - {self:?}");
-                StatusCode::INTERNAL_SERVER_ERROR.into_response()
-            }
+            Self::ValidationErrors(e) => {
+                tracing::info!("Bad request - {e:?}");
+                (StatusCode::BAD_REQUEST, e.to_string()).into_response()
+            }
+            Self::ValidationError(e) => {
+                tracing::info!("Bad request - {e:?}");
+                (StatusCode::BAD_REQUEST, e.to_string()).into_response()
+            }
+            _ => {
+                tracing::error!("Internal server error - {self:?}");
+                StatusCode::INTERNAL_SERVER_ERROR.into_response()
+            }

If you prefer JSON, I can wire (StatusCode::BAD_REQUEST, axum::Json(...)) instead.

src/domain/subscriber/mod.rs (1)

16-46: Decouple domain from handlers (avoid TryFrom in domain).

Having domain depend on crate::handlers is an unnecessary layering coupling. Prefer a domain-level conversion (e.g., from raw strings) and keep FormData-to-domain mapping in the handlers layer.

Apply this refactor within this file:

- use crate::handlers::FormData;
+// Keep domain independent from the transport layer

-impl TryFrom<FormData> for Subscriber {
+impl TryFrom<(String, String)> for Subscriber {
     type Error = validator::ValidationErrors;

-    fn try_from(value: FormData) -> Result<Self, Self::Error> {
+    fn try_from((email_raw, name_raw): (String, String)) -> Result<Self, Self::Error> {
         let mut errors = ValidationErrors::new();

-        let email: Option<SubscriberEmail> = match value.email.try_into() {
+        let email: Option<SubscriberEmail> = match email_raw.try_into() {
             Ok(email) => Some(email),
             Err(error) => {
                 errors.add("email", error);
                 None
             }
         };

-        let name: Option<SubscriberName> = match value.name.try_into() {
+        let name: Option<SubscriberName> = match name_raw.try_into() {
             Ok(name) => Some(name),
             Err(error) => {
                 errors.add("name", error);
                 None
             }
         };

Then, in handlers, construct with Subscriber::try_from((form.email, form.name)). This retains your detailed validation errors (per prior preference to include the actual email in messages) while fixing the dependency direction.

schemas/subscriptions.surql (2)

9-9: Consider making token explicitly optional.

Unless every new row sets token at creation time, model this as optional to avoid accidental type errors during inserts.

-DEFINE FIELD OVERWRITE token ON subscriptions TYPE record<subscription_tokens>;
+DEFINE FIELD OVERWRITE token ON subscriptions TYPE option<record<subscription_tokens>>;

6-11: Minor style consistency: use “ON TABLE” uniformly.

Cosmetic only; improves readability.

-DEFINE FIELD OVERWRITE email ON subscriptions TYPE string ASSERT string::is::email($value);
-DEFINE FIELD OVERWRITE name ON subscriptions TYPE string;
-DEFINE FIELD OVERWRITE status ON subscriptions TYPE 'PENDING' | 'CONFIRMED' DEFAULT 'PENDING';
-DEFINE FIELD OVERWRITE token ON subscriptions TYPE record<subscription_tokens>;
+DEFINE FIELD OVERWRITE email ON TABLE subscriptions TYPE string ASSERT string::is::email($value);
+DEFINE FIELD OVERWRITE name ON TABLE subscriptions TYPE string;
+DEFINE FIELD OVERWRITE status ON TABLE subscriptions TYPE 'PENDING' | 'CONFIRMED' DEFAULT 'PENDING';
+DEFINE FIELD OVERWRITE token ON TABLE subscriptions TYPE record<subscription_tokens>;
.env.example (1)

6-6: Keep .env.example in sync: order keys and add TIMEOUT for email client.

Reorder to satisfy dotenv-linter and include the missing timeout to mirror code/tests.

Apply:

 # Subscriptions Application
 SUBSCRIPTIONS__PORT=1337
-SUBSCRIPTIONS__HOST=0.0.0.0
-SUBSCRIPTIONS__BASE_URL=http://localhost:1337
+SUBSCRIPTIONS__BASE_URL=http://localhost:1337
+SUBSCRIPTIONS__HOST=0.0.0.0

 # Subscriptions Database
-SUBSCRIPTIONS__DATABASE__USERNAME=subscriptions
-SUBSCRIPTIONS__DATABASE__PASSWORD=password
-SUBSCRIPTIONS__DATABASE__NAMESPACE=main
-SUBSCRIPTIONS__DATABASE__NAME=db
+SUBSCRIPTIONS__DATABASE__NAME=db
+SUBSCRIPTIONS__DATABASE__NAMESPACE=main
+SUBSCRIPTIONS__DATABASE__PASSWORD=password
+SUBSCRIPTIONS__DATABASE__USERNAME=subscriptions

 # Subscriptions Email Client
-SUBSCRIPTIONS__EMAIL_CLIENT__SENDER_EMAIL=
-SUBSCRIPTIONS__EMAIL_CLIENT__BASE_URL=
-SUBSCRIPTIONS__EMAIL_CLIENT__AUTH_TOKEN=
+SUBSCRIPTIONS__EMAIL_CLIENT__AUTH_TOKEN=
+SUBSCRIPTIONS__EMAIL_CLIENT__BASE_URL=
+SUBSCRIPTIONS__EMAIL_CLIENT__SENDER_EMAIL=
+SUBSCRIPTIONS__EMAIL_CLIENT__TIMEOUT=10s

Also applies to: 9-13, 15-19

src/handlers/health_check.rs (1)

4-4: Prefer http::StatusCode over reqwest::StatusCode in handlers.

Avoid coupling the handler to reqwest; use Axum/http’s StatusCode.

-use reqwest::StatusCode;
+use axum::http::StatusCode;
src/main.rs (1)

24-29: Avoid cloning Config; capture host/port before moving it into init.

Small perf/readability win; no behavior change.

-    let (router, _) = subscriptions::init(config.clone())
+    let host = config.host.clone();
+    let port = config.port;
+    let (router, _) = subscriptions::init(config)
         .await
         .unwrap_or_else(|error| {
             tracing::error!(%error, "Failed to initialize application");
             process::exit(1);
         });
 
     // Bind address
-    let listener = TcpListener::bind((config.host.clone(), config.port))
+    let listener = TcpListener::bind((host.clone(), port))
         .await
         .unwrap_or_else(|error| {
-            tracing::error!(%error, "Failed to bind address `{}:{}`", config.host, config.port);
+            tracing::error!(%error, "Failed to bind address `{}:{}`", host, port);
             process::exit(1);
         });
 
     // Start server
-    tracing::info!("Start listening on: http://{}:{}", config.host, config.port);
+    tracing::info!("Start listening on: http://{}:{}", host, port);

Also applies to: 31-41

.env.test (1)

1-1: Fix lint issues and a typo; quote values and order keys.

Quotes prevent parser surprises; ordering silences dotenv-linter; fix “overided” → “overridden”.

-RUST_LOG=debug # https://docs.rs/env_logger/latest/env_logger/#enabling-logging
+RUST_LOG="debug" # https://docs.rs/env_logger/latest/env_logger/#enabling-logging

 # Subscriptions Application
-SUBSCRIPTIONS__PORT=1337 # not used by tests
-SUBSCRIPTIONS__HOST=0.0.0.0 # not used by tests
-SUBSCRIPTIONS__BASE_URL=http://localhost:1337 # not used by tests
+SUBSCRIPTIONS__PORT="1337" # not used by tests
+SUBSCRIPTIONS__HOST="0.0.0.0" # not used by tests
+SUBSCRIPTIONS__BASE_URL="http://localhost:1337" # not used by tests

 # Subscriptions Database
 SUBSCRIPTIONS__DATABASE__BASE_URL=mem://
-SUBSCRIPTIONS__DATABASE__USERNAME=subscriptions
-SUBSCRIPTIONS__DATABASE__PASSWORD=password
-SUBSCRIPTIONS__DATABASE__NAMESPACE=main
-SUBSCRIPTIONS__DATABASE__NAME=db
+SUBSCRIPTIONS__DATABASE__NAME=db
+SUBSCRIPTIONS__DATABASE__NAMESPACE=main
+SUBSCRIPTIONS__DATABASE__PASSWORD=password
+SUBSCRIPTIONS__DATABASE__USERNAME=subscriptions

 # Subscriptions Email Client
-SUBSCRIPTIONS__EMAIL_CLIENT__SENDER_EMAIL=admin@example.com
-SUBSCRIPTIONS__EMAIL_CLIENT__BASE_URL=http://example.com/path # overided with mocked url by tests
-SUBSCRIPTIONS__EMAIL_CLIENT__AUTH_TOKEN=token
-SUBSCRIPTIONS__EMAIL_CLIENT__TIMEOUT=1s
+SUBSCRIPTIONS__EMAIL_CLIENT__AUTH_TOKEN="token"
+SUBSCRIPTIONS__EMAIL_CLIENT__BASE_URL="http://example.com/path" # overridden with mocked url by tests
+SUBSCRIPTIONS__EMAIL_CLIENT__SENDER_EMAIL="admin@example.com"
+SUBSCRIPTIONS__EMAIL_CLIENT__TIMEOUT="1s"

Also applies to: 4-6, 10-13, 16-19

tests/api/subscriptions_confirm.rs (2)

20-21: Consistency: terminate the assert with a semicolon.

Keep style consistent with the rest of the file.

-    assert_eq!(response.status_code(), StatusCode::BAD_REQUEST)
+    assert_eq!(response.status_code(), StatusCode::BAD_REQUEST);

39-39: Prefer expect over unwrap and avoid hard indexing.

Improves failure messages and avoids panics on empty collections.

-    let email_request = &app.email_server.received_requests().await.unwrap()[0];
+    let email_request = app
+        .email_server
+        .received_requests()
+        .await
+        .expect("failed to fetch received requests")
+        .first()
+        .expect("email request was not captured");
src/config.rs (1)

30-30: Unify SecretString import usage.

You import SecretString already—use it consistently.

-    pub password: secrecy::SecretString,
+    pub password: SecretString,
src/state.rs (1)

17-23: async not needed on AppState::new.

No awaits inside; make it sync to simplify call sites.

-impl AppState {
-    pub async fn new(config: Config) -> Result<Self> {
+impl AppState {
+    pub fn new(config: Config) -> Result<Self> {
         Ok(Self {
             mm: Arc::new(model::ModelManager::new(config.database.clone())),
             email_client: Arc::new(EmailClient::new(config.email_client.clone())?),
             config: Arc::new(config),
         })
     }
 }

Follow-up: update call sites removing .await.

src/model.rs (3)

33-44: Avoid unnecessary String allocations in binds.

You can bind &str directly; no need for to_string() on hot paths.

-            .bind(("token_val", token.to_string()))
-            .bind(("email", subscriber.email.as_ref().to_string()))
-            .bind(("name", subscriber.name.as_ref().to_string()))
+            .bind(("token_val", token))
+            .bind(("email", subscriber.email.as_ref()))
+            .bind(("name", subscriber.name.as_ref()))

Also applies to: 45-46


35-41: Consider returning nothing from the transactional query.

If you don't consume results, append RETURN NONE to reduce payload/decoding.

                 CREATE subscriptions CONTENT {
                     email: $email,
                     name: $name,
                     token: $subscription_token.id
                 };
-                COMMIT TRANSACTION;
+                COMMIT TRANSACTION;
+                RETURN NONE;

57-61: Quote username in mem bootstrap to avoid identifier issues.

Backtick-quote to handle non-alphanumeric usernames in DEFINE USER.

-                .query(format!(
-                    "DEFINE USER {} ON DATABASE PASSWORD '{}' ROLES OWNER",
-                    config.username,
-                    config.password.expose_secret()
-                ))
+                .query(format!(
+                    "DEFINE USER `{}` ON DATABASE PASSWORD '{}' ROLES OWNER",
+                    config.username,
+                    config.password.expose_secret()
+                ))

Also applies to: 60-70

migrations/definitions/_initial.json (1)

1-1: Add minimal token hardness constraint.

Prevent empty/short tokens to reduce accidental duplicates before hashing/encoding.

-DEFINE FIELD OVERWRITE token ON subscription_tokens TYPE string;
+DEFINE FIELD OVERWRITE token ON subscription_tokens TYPE string ASSERT string::len($value) > 15;
tests/api/subscriptions.rs (2)

145-146: Typos in assertion comment.

Clarify comment; no behavior change.

-    // the two link should be adentical
+    // The two links should be identical

150-150: Test name/status mismatch.

Name says 200 but you assert 201. Rename for clarity or change expected status.

-pub async fn subscribe_returns_a_200_for_valid_form_data() {
+pub async fn subscribe_returns_a_201_for_valid_form_data() {

Also applies to: 167-167

src/email_client.rs (4)

36-36: Fix typo: rename recipeint to recipient.

Keeps naming consistent and prevents future confusion.

-        recipeint: &SubscriberEmail,
+        recipient: &SubscriberEmail,
...
-            to: recipeint.as_ref(),
+            to: recipient.as_ref(),

Also applies to: 43-43


109-116: Ensure header matcher uses a string name.

wiremock::matchers::header_exists expects a string; pass as_str() to avoid type mismatch.

-        Mock::given(header_exists(EMAIL_CLIENT_AUTH_HEADER))
+        Mock::given(header_exists(EMAIL_CLIENT_AUTH_HEADER.as_str()))
             .and(header(CONTENT_TYPE, mime::APPLICATION_JSON.as_ref()))
             .and(method(Method::POST))
             .and(SendEmailBodyMatcher)

170-170: Fix test name typo.

“too long”, not “to long”.

-    async fn send_email_timeout_if_the_server_takes_to_long() {
+    async fn send_email_timeouts_if_the_server_takes_too_long() {

211-211: Remove redundant .into().

Faker.fake::<String>() already returns String.

-            auth_token: SecretString::new(Faker.fake::<String>().into()),
+            auth_token: SecretString::new(Faker.fake::<String>()),
tests/api/helpers.rs (1)

59-65: Guard against missing links in tests.

Add an assertion before indexing to make failures clearer.

-            let raw_link = links[0].as_str().to_owned();
+            assert!(
+                !links.is_empty(),
+                "Expected at least one URL in the email body, found none"
+            );
+            let raw_link = links[0].as_str().to_owned();
src/handlers/subscription.rs (3)

19-25: Avoid logging PII in spans.

Skip form in the instrument macro to avoid capturing user email/name.

-#[tracing::instrument(skip(mm, config, email_client))]
+#[tracing::instrument(skip(mm, config, email_client, form))]

9-9: Use http::StatusCode instead of pulling it from reqwest.

Reduces coupling between handlers and the HTTP client crate.

-use reqwest::StatusCode;
+use axum::http::StatusCode;

13-17: Restrict visibility of FormData fields.

These don’t need to be pub for extractors; keep them private unless used externally.

 #[derive(Debug, Deserialize)]
 pub struct FormData {
-    pub email: String,
-    pub name: String,
+    email: String,
+    name: String,
 }
📜 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 533e8dd and 5996bdb.

⛔ Files ignored due to path filters (1)
  • Cargo.lock is excluded by !**/*.lock
📒 Files selected for processing (29)
  • .env.example (1 hunks)
  • .env.test (1 hunks)
  • .github/workflows/cd.yml (1 hunks)
  • .moon/tasks/rust-backend-application.yml (1 hunks)
  • Cargo.toml (1 hunks)
  • migrations/definitions/_initial.json (1 hunks)
  • schemas/subscription_tokens.surql (1 hunks)
  • schemas/subscriptions.surql (1 hunks)
  • src/config.rs (1 hunks)
  • src/domain/mod.rs (1 hunks)
  • src/domain/subscriber/email.rs (1 hunks)
  • src/domain/subscriber/mod.rs (1 hunks)
  • src/email_client.rs (1 hunks)
  • src/errors.rs (1 hunks)
  • src/handlers.rs (0 hunks)
  • src/handlers/health_check.rs (1 hunks)
  • src/handlers/mod.rs (1 hunks)
  • src/handlers/subscription.rs (1 hunks)
  • src/lib.rs (1 hunks)
  • src/main.rs (1 hunks)
  • src/model.rs (2 hunks)
  • src/startup.rs (3 hunks)
  • src/state.rs (2 hunks)
  • tests/api/health_check.rs (1 hunks)
  • tests/api/helpers.rs (1 hunks)
  • tests/api/main.rs (1 hunks)
  • tests/api/subscriptions.rs (1 hunks)
  • tests/api/subscriptions_confirm.rs (1 hunks)
  • tests/tests.rs (0 hunks)
💤 Files with no reviewable changes (2)
  • src/handlers.rs
  • tests/tests.rs
🧰 Additional context used
🧠 Learnings (1)
📚 Learning: 2025-08-28T13:50:40.460Z
Learnt from: TN19N
PR: TN19N/subscriptions#9
File: src/domain/subscriber/email.rs:18-19
Timestamp: 2025-08-28T13:50:40.460Z
Learning: For the SubscriberEmail validation in src/domain/subscriber/email.rs, the user TN19N prefers to include the actual email value in ValidationError messages for debugging purposes, accepting the PII exposure trade-off in favor of detailed error information returned with 400 HTTP status.

Applied to files:

  • src/domain/mod.rs
  • src/domain/subscriber/mod.rs
  • src/domain/subscriber/email.rs
🧬 Code graph analysis (17)
src/handlers/health_check.rs (3)
src/handlers/subscription.rs (1)
  • mm (47-64)
src/handlers.rs (2)
  • health (25-29)
  • subscribe (14-22)
tests/tests.rs (1)
  • health_works (37-57)
src/lib.rs (1)
src/email_client.rs (1)
  • email_client (207-215)
tests/api/health_check.rs (3)
tests/api/helpers.rs (1)
  • new (24-51)
tests/tests.rs (2)
  • health_works (37-57)
  • init (16-34)
src/handlers.rs (1)
  • health (25-29)
src/handlers/mod.rs (1)
src/handlers.rs (2)
  • subscribe (14-22)
  • health (25-29)
tests/api/main.rs (1)
tests/tests.rs (3)
  • health_works (37-57)
  • subscribe_works (60-105)
  • subscribe_failed (108-142)
tests/api/subscriptions_confirm.rs (3)
tests/api/subscriptions.rs (2)
  • app (30-39)
  • app (175-184)
src/state.rs (1)
  • new (17-23)
tests/api/helpers.rs (1)
  • new (24-51)
.env.test (1)
tests/tests.rs (3)
  • subscribe_works (60-105)
  • subscribe_failed (108-142)
  • dotenvy (20-29)
src/startup.rs (2)
src/handlers/subscription.rs (2)
  • confirm (43-67)
  • subscribe (20-35)
tests/tests.rs (1)
  • init (16-34)
src/state.rs (3)
src/email_client.rs (2)
  • email_client (207-215)
  • new (26-32)
src/model.rs (1)
  • new (14-19)
tests/api/helpers.rs (1)
  • new (24-51)
tests/api/helpers.rs (5)
src/email_client.rs (1)
  • new (26-32)
src/state.rs (1)
  • new (17-23)
src/config.rs (1)
  • load (36-41)
src/startup.rs (1)
  • init (20-43)
tests/tests.rs (2)
  • Result (14-14)
  • init (16-34)
tests/api/subscriptions.rs (4)
tests/api/subscriptions_confirm.rs (1)
  • app (57-66)
src/state.rs (1)
  • new (17-23)
tests/api/helpers.rs (1)
  • new (24-51)
src/handlers.rs (1)
  • subscribe (14-22)
src/domain/subscriber/mod.rs (1)
src/email_client.rs (1)
  • email (201-204)
src/domain/subscriber/email.rs (2)
tests/tests.rs (1)
  • subscribe_works (60-105)
src/handlers.rs (1)
  • FormData (8-11)
src/email_client.rs (3)
src/state.rs (1)
  • new (17-23)
tests/api/helpers.rs (1)
  • new (24-51)
src/domain/subscriber/email.rs (1)
  • try_from (17-23)
src/model.rs (2)
src/handlers.rs (1)
  • subscribe (14-22)
tests/tests.rs (1)
  • subscribe_works (60-105)
src/handlers/subscription.rs (3)
src/email_client.rs (1)
  • email_client (207-215)
src/handlers.rs (2)
  • subscribe (14-22)
  • FormData (8-11)
tests/tests.rs (1)
  • subscribe_works (60-105)
src/main.rs (1)
src/startup.rs (1)
  • init (20-43)
🪛 dotenv-linter (3.3.0)
.env.test

[warning] 1-1: [ValueWithoutQuotes] This value needs to be surrounded in quotes

(ValueWithoutQuotes)


[warning] 4-4: [ValueWithoutQuotes] This value needs to be surrounded in quotes

(ValueWithoutQuotes)


[warning] 5-5: [UnorderedKey] The SUBSCRIPTIONS__HOST key should go before the SUBSCRIPTIONS__PORT key

(UnorderedKey)


[warning] 5-5: [ValueWithoutQuotes] This value needs to be surrounded in quotes

(ValueWithoutQuotes)


[warning] 6-6: [UnorderedKey] The SUBSCRIPTIONS__BASE_URL key should go before the SUBSCRIPTIONS__HOST key

(UnorderedKey)


[warning] 6-6: [ValueWithoutQuotes] This value needs to be surrounded in quotes

(ValueWithoutQuotes)


[warning] 11-11: [UnorderedKey] The SUBSCRIPTIONS__DATABASE__PASSWORD key should go before the SUBSCRIPTIONS__DATABASE__USERNAME key

(UnorderedKey)


[warning] 12-12: [UnorderedKey] The SUBSCRIPTIONS__DATABASE__NAMESPACE key should go before the SUBSCRIPTIONS__DATABASE__PASSWORD key

(UnorderedKey)


[warning] 13-13: [UnorderedKey] The SUBSCRIPTIONS__DATABASE__NAME key should go before the SUBSCRIPTIONS__DATABASE__NAMESPACE key

(UnorderedKey)


[warning] 17-17: [UnorderedKey] The SUBSCRIPTIONS__EMAIL_CLIENT__BASE_URL key should go before the SUBSCRIPTIONS__EMAIL_CLIENT__SENDER_EMAIL key

(UnorderedKey)


[warning] 17-17: [ValueWithoutQuotes] This value needs to be surrounded in quotes

(ValueWithoutQuotes)


[warning] 18-18: [UnorderedKey] The SUBSCRIPTIONS__EMAIL_CLIENT__AUTH_TOKEN key should go before the SUBSCRIPTIONS__EMAIL_CLIENT__BASE_URL key

(UnorderedKey)

.env.example

[warning] 6-6: [UnorderedKey] The SUBSCRIPTIONS__BASE_URL key should go before the SUBSCRIPTIONS__HOST key

(UnorderedKey)


[warning] 11-11: [UnorderedKey] The SUBSCRIPTIONS__DATABASE__PASSWORD key should go before the SUBSCRIPTIONS__DATABASE__USERNAME key

(UnorderedKey)


[warning] 12-12: [UnorderedKey] The SUBSCRIPTIONS__DATABASE__NAMESPACE key should go before the SUBSCRIPTIONS__DATABASE__PASSWORD key

(UnorderedKey)


[warning] 13-13: [UnorderedKey] The SUBSCRIPTIONS__DATABASE__NAME key should go before the SUBSCRIPTIONS__DATABASE__NAMESPACE key

(UnorderedKey)


[warning] 17-17: [UnorderedKey] The SUBSCRIPTIONS__EMAIL_CLIENT__BASE_URL key should go before the SUBSCRIPTIONS__EMAIL_CLIENT__SENDER_EMAIL key

(UnorderedKey)


[warning] 18-18: [UnorderedKey] The SUBSCRIPTIONS__EMAIL_CLIENT__AUTH_TOKEN key should go before the SUBSCRIPTIONS__EMAIL_CLIENT__BASE_URL key

(UnorderedKey)

⏰ 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). (1)
  • GitHub Check: check
🔇 Additional comments (20)
.github/workflows/cd.yml (1)

74-82: Confirm new env var wiring and secrets presence

New vars look consistent with the config changes. Please verify repo/environ has these defined for the production environment to avoid runtime failures.

src/domain/subscriber/email.rs (1)

20-21: Good: error message includes the offending email

Matches your stated preference for detailed validation errors.

Cargo.toml (2)

9-15: Double-check panic strategy

panic = 'abort' reduces size but will kill the process on panic; ensure this aligns with your SLOs on Cloud Run (restarts, cold starts).


38-46: Deps look consistent with the new email client and tests

Moving claims to dev-deps and adding wiremock/axum-test/linkify aligns with the new integration tests.

tests/api/health_check.rs (1)

1-16: LGTM

Test is concise and asserts both status and empty body correctly using the TestApp harness.

src/domain/mod.rs (1)

4-4: LGTM

Re-exporting SubscriberEmail from the domain root improves ergonomics for callers.

src/lib.rs (1)

3-3: Private email_client module wiring looks good.

Module added without leaking public API; aligns with AppState holding Arc.

tests/api/main.rs (1)

1-4: Test harness module aggregation is clean.

Straightforward, discoverable structure for the new integration tests.

src/domain/subscriber/mod.rs (1)

4-5: Public re-export of SubscriberEmail is appropriate.

This simplifies downstream imports (crate::domain::SubscriberEmail) without exposing the whole submodule.

src/handlers/mod.rs (1)

1-5: Handlers re-export pattern is tidy.

Clear split between health_check and subscription; the glob re-exports keep router wiring concise.

schemas/subscriptions.surql (1)

8-9: Literal enum and record type usage are valid.

  • Using a literal union for status is supported: TYPE "PENDING" | "CONFIRMED". (surrealdb.com)
  • record<subscription_tokens> is a valid field type pattern. (surrealdb.com)
src/handlers/health_check.rs (1)

7-12: LGTM: DB health check returns 200 with empty body.

Implementation is correct and matches tests’ expectations.

src/startup.rs (2)

38-38: LGTM: added GET /subscriptions/confirm route.

Route wiring is correct and consistent with the new handler.


20-20: No outdated call sites: all init calls updated to take Config by value.

src/config.rs (2)

36-41: Config loader looks good.

Builder + env source + try_deserialize() is clean.


22-23: No change needed: serde_humantime matches your Cargo.toml
Your Cargo.toml declares serde-humantime = "0.1.1", and the crate’s docs show using #[serde(with = "serde_humantime")] (docs.rs)

Likely an incorrect or invalid review comment.

src/state.rs (1)

32-42: Nice use of FromRef for Axum extractors.

Clean dependency injection with Arc clones.

src/model.rs (1)

25-29: All create_subscriber call sites updated
Only usage in src/handlers/subscription.rs already calls mm.create_subscriber(&subscriber, &token), matching the new signature.

tests/api/subscriptions.rs (1)

56-87: Status codes for invalid payloads: verify contract.

Mix of 422 and 400—ensure handlers map validation errors consistently.

If needed, align handler responses or adjust expected codes in these cases.

Also applies to: 89-100

tests/api/helpers.rs (1)

53-53: Rename get_conformation_links to get_confirmation_links
tests/api/helpers.rs:53

-    pub fn get_conformation_links(&self, email_request: &wiremock::Request) -> ConfirmationLinks {
+    pub fn get_confirmation_links(&self, email_request: &wiremock::Request) -> ConfirmationLinks {

Update call sites accordingly:

  • tests/api/subscriptions_confirm.rs:40
  • tests/api/subscriptions.rs:144

Comment on lines +20 to 21
reqwest = { version = "0.12.23", features = ["json", "rustls-tls"] }
secrecy = { version = "0.10.3", features = ["serde"] }
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Make reqwest use only rustls (no accidental default features)

Without default-features = false, reqwest’s defaults may pull in TLS you don’t want.

Apply:

-reqwest = { version = "0.12.23", features = ["json", "rustls-tls"] }
+reqwest = { version = "0.12.23", default-features = false, features = ["json", "rustls-tls"] }
🤖 Prompt for AI Agents
In Cargo.toml around lines 20 to 21, the reqwest dependency currently enables
default features which can pull in unwanted TLS/backends; update the reqwest
entry to disable default features and explicitly enable rustls by setting
default-features = false and keeping features = ["json", "rustls-tls"] so only
rustls TLS is used.

Comment on lines +6 to +10
DEFINE FIELD OVERWRITE token ON subscription_tokens TYPE string;
DEFINE FIELD OVERWRITE created_at ON TABLE subscription_tokens TYPE datetime VALUE time::now() READONLY;

# --- INDEXES ---
DEFINE INDEX OVERWRITE unique_token ON subscription_tokens COLUMNS token UNIQUE;
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Enforce non-empty tokens at the schema level

Prevent empty-string tokens; uniqueness alone still permits one empty token.

Apply:

-DEFINE FIELD OVERWRITE token ON subscription_tokens TYPE string;
+DEFINE FIELD OVERWRITE token ON subscription_tokens
+    TYPE string
+    ASSERT string::len($value) > 0;
📝 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
DEFINE FIELD OVERWRITE token ON subscription_tokens TYPE string;
DEFINE FIELD OVERWRITE created_at ON TABLE subscription_tokens TYPE datetime VALUE time::now() READONLY;
# --- INDEXES ---
DEFINE INDEX OVERWRITE unique_token ON subscription_tokens COLUMNS token UNIQUE;
DEFINE FIELD OVERWRITE token ON subscription_tokens
TYPE string
ASSERT string::len($value) > 0;
DEFINE FIELD OVERWRITE created_at ON TABLE subscription_tokens TYPE datetime VALUE time::now() READONLY;
# --- INDEXES ---
DEFINE INDEX OVERWRITE unique_token ON subscription_tokens COLUMNS token UNIQUE;
🤖 Prompt for AI Agents
In schemas/subscription_tokens.surql around lines 6 to 10, the token field
allows an empty string which the UNIQUE index won't prevent; update the token
field definition to enforce non-empty values at the schema level—add a NOT EMPTY
/ REQUIRED constraint or an explicit CHECK/ASSERT that token != '' or
length(token) > 0 (depending on SurQL syntax) so empty-string tokens are
rejected while keeping the unique index intact.

Comment on lines +1 to 6
use serde::Deserialize;
use std::convert::TryFrom;
use validator::{ValidateEmail, ValidationError};

#[derive(Debug)]
#[derive(Debug, Clone, Deserialize)]
pub struct SubscriberEmail(String);
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Route serde deserialization through TryFrom to enforce validation

Deriving Deserialize on the newtype bypasses your TryFrom checks. Make serde call TryFrom so invalid emails are rejected at config/JSON parse time.

Apply:

-use serde::Deserialize;
+use serde::{Deserialize, Serialize};
@@
-#[derive(Debug, Clone, Deserialize)]
+#[derive(Debug, Clone, Serialize, Deserialize)]
+#[serde(try_from = "String", into = "String")]
 pub struct SubscriberEmail(String);
@@
 impl TryFrom<String> for SubscriberEmail {
@@
 }
+
+impl From<SubscriberEmail> for String {
+    fn from(value: SubscriberEmail) -> Self {
+        value.0
+    }
+}

Also applies to: 14-24

Comment on lines +51 to +61
r#"
UPDATE subscriptions
SET status = 'CONFIRMED'
WHERE token = (
SELECT id
FROM ONLY subscription_tokens
WHERE token = $token_val
).id
AND status = 'PENDING';
"#,
)
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue

Fix confirmation UPDATE predicate (wrong column).

You’re comparing subscriptions.token to a selected id. This will never match unless a token column exists and equals an id. Compare id to the selected id.

-        UPDATE subscriptions
-        SET status = 'CONFIRMED'
-        WHERE token = (
-            SELECT id
-            FROM ONLY subscription_tokens
-            WHERE token = $token_val
-        ).id
-        AND status = 'PENDING';
+        UPDATE subscriptions
+        SET status = 'CONFIRMED'
+        WHERE id = (
+            SELECT id
+            FROM ONLY subscription_tokens
+            WHERE token = $token_val
+        ).id
+        AND status = 'PENDING';
📝 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
r#"
UPDATE subscriptions
SET status = 'CONFIRMED'
WHERE token = (
SELECT id
FROM ONLY subscription_tokens
WHERE token = $token_val
).id
AND status = 'PENDING';
"#,
)
r#"
UPDATE subscriptions
SET status = 'CONFIRMED'
WHERE id = (
SELECT id
FROM ONLY subscription_tokens
WHERE token = $token_val
).id
AND status = 'PENDING';
"#,
)
🤖 Prompt for AI Agents
In src/handlers/subscription.rs around lines 51 to 61, the UPDATE currently
compares subscriptions.token to the selected id from subscription_tokens which
will never match; change the predicate to compare subscriptions.id to the
selected id (e.g. WHERE id = (SELECT id FROM ONLY subscription_tokens WHERE
token = $token_val) AND status = 'PENDING') so the subscription row is correctly
matched and updated.

Comment on lines +69 to +75
fn get_confirmation_token() -> String {
let mut rng = rand::rng();
std::iter::repeat_with(|| rng.sample(Alphanumeric))
.map(char::from)
.take(25)
.collect()
}
Copy link
Contributor

Choose a reason for hiding this comment

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

💡 Verification agent

🧩 Analysis chain

Confirm rand API compatibility (rng() and distr::Alphanumeric).

If the crate uses rand 0.8, this won’t compile (needs thread_rng() and distributions::Alphanumeric). Verify the version and adjust if needed.


🏁 Script executed:

#!/bin/bash
# Inspect rand usage/version
rg -n "rand" -C2
fd Cargo.toml | xargs -I{} rg -n "rand\s*=" {}

Length of output: 10342


Update rand API usage: Replace rand::rng() with rand::thread_rng() and import Alphanumeric from rand::distributions instead of rand::distr.

--- a/src/handlers/subscription.rs
+++ b/src/handlers/subscription.rs
-use rand::Rng;
-use rand::distr::Alphanumeric;
+use rand::{Rng, thread_rng, distributions::Alphanumeric};

 fn get_confirmation_token() -> String {
-    let mut rng = rand::rng();
+    let mut rng = thread_rng();
     std::iter::repeat_with(|| rng.sample(Alphanumeric))
         .map(char::from)
         .take(25)
         .collect()
 }
📝 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
fn get_confirmation_token() -> String {
let mut rng = rand::rng();
std::iter::repeat_with(|| rng.sample(Alphanumeric))
.map(char::from)
.take(25)
.collect()
}
-use rand::Rng;
use rand::{Rng, thread_rng, distributions::Alphanumeric};
fn get_confirmation_token() -> String {
let mut rng = thread_rng();
std::iter::repeat_with(|| rng.sample(Alphanumeric))
.map(char::from)
.take(25)
.collect()
}
🤖 Prompt for AI Agents
In src/handlers/subscription.rs around lines 69 to 75, update the rand API
usage: replace the call to rand::rng() with rand::thread_rng(), and change the
import for Alphanumeric from rand::distr to rand::distributions::Alphanumeric;
ensure you still have the Rng trait in scope so rng.sample(Alphanumeric) (or
sample_iter) continues to work. Make only these substitutions so token
generation remains the same but uses the correct current rand API.

@@ -0,0 +1,75 @@
use crate::helpers::TestApp;
use reqwest::{Method, StatusCode};
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue

Wiremock matcher and response status type mismatch (won’t compile).

Use string-based method matcher and a u16 status; drop the unused Method import.

Apply:

-use reqwest::{Method, StatusCode};
+use reqwest::StatusCode;
@@
-    Mock::given(any())
-        .and(method(Method::POST))
-        .respond_with(ResponseTemplate::new(StatusCode::OK))
+    Mock::given(any())
+        .and(method("POST"))
+        .respond_with(ResponseTemplate::new(200))
         .mount(&app.email_server)
         .await;

Also applies to: 31-35

🤖 Prompt for AI Agents
In tests/api/subscriptions_confirm.rs around line 2 (and similarly lines 31-35),
the Wiremock setup uses the strongly typed reqwest::Method and an integer
response StatusCode type which causes a type mismatch; remove the unused Method
import, change the Wiremock method matcher to use a string (e.g. "POST" or
"GET") instead of reqwest::Method, and set the mock response status to a u16
(e.g. 200u16) or call .status(200) with a numeric literal so the response uses a
primitive integer status; update any other Wiremock matchers in lines 31-35 the
same way.

.await;

// Act
_ = app.server.post("/subscriptions").form(&body).await;
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue

Missing let before _ sink variable.

Current line is invalid Rust; add let.

-    _ = app.server.post("/subscriptions").form(&body).await;
+    let _ = app.server.post("/subscriptions").form(&body).await;
📝 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
_ = app.server.post("/subscriptions").form(&body).await;
let _ = app.server.post("/subscriptions").form(&body).await;
🤖 Prompt for AI Agents
In tests/api/subscriptions_confirm.rs around line 38, the statement "_ =
app.server.post(\"/subscriptions\").form(&body).await;" is invalid Rust because
the assignment target is missing the "let" binding; update the line to include
"let" (e.g., "let _ = ...") so the await result is properly bound and the code
compiles.

// Act
_ = app.server.post("/subscriptions").form(&body).await;
let email_request = &app.email_server.received_requests().await.unwrap()[0];
let confirmation_links = app.get_conformation_links(email_request);
Copy link
Contributor

Choose a reason for hiding this comment

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

💡 Verification agent

🧩 Analysis chain

Typo: “conformation” → “confirmation”.

Rename for clarity and consistency with helpers.

If the helper is actually named get_confirmation_links, apply:

-    let confirmation_links = app.get_conformation_links(email_request);
+    let confirmation_links = app.get_confirmation_links(email_request);

To confirm the helper name, run:


🏁 Script executed:

#!/bin/bash
rg -nP 'fn\s+get_?confi?r?mation_links' -C2 tests/api helpers src

Length of output: 115


🏁 Script executed:

#!/bin/bash
# Search for typo and correct helper definitions/usages across the entire repo
rg -n 'get_conformation_links' -C2 .
rg -n 'get_confirmation_links' -C2 .

Length of output: 1349


Rename get_conformation_links to get_confirmation_links across tests/api/

  • In tests/api/helpers.rs (line 53), change
    -pub fn get_conformation_links(&self, email_request: &wiremock::Request) -> ConfirmationLinks {
    +pub fn get_confirmation_links(&self, email_request: &wiremock::Request) -> ConfirmationLinks {
  • In tests/api/subscriptions_confirm.rs (line 40) and tests/api/subscriptions.rs (line 144), update calls to:
    let confirmation_links = app.get_confirmation_links(email_request);
🤖 Prompt for AI Agents
In tests/api/subscriptions_confirm.rs around line 40 the test calls the
misspelled helper get_conformation_links; rename the helper to
get_confirmation_links in tests/api/helpers.rs (line 53) and update all call
sites (including tests/api/subscriptions_confirm.rs line 40 and
tests/api/subscriptions.rs line 144) to use
app.get_confirmation_links(email_request) so names match and tests compile.

Comment on lines +17 to +22
Mock::given(any())
.and(method(Method::POST))
.respond_with(ResponseTemplate::new(StatusCode::OK))
.expect(1)
.mount(&app.email_server)
.await;
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue

Fix wiremock method matcher type.

matchers::method expects a string; using reqwest::Method won’t compile.

-use reqwest::{Method, StatusCode};
+use reqwest::StatusCode;
...
-    Mock::given(any())
-        .and(method(Method::POST))
+    Mock::given(any())
+        .and(method("POST"))
         .respond_with(ResponseTemplate::new(StatusCode::OK))

Apply the same change to all mocks in this file.

Also applies to: 110-116, 132-137, 157-161

🤖 Prompt for AI Agents
In tests/api/subscriptions.rs around lines 17-22 (and similarly at 110-116,
132-137, 157-161), the wiremock matcher call uses reqwest::Method (e.g.
Method::POST) but matchers::method expects a string; replace the Method::XXX
usages with string literals like "POST", "GET", etc. in each
Mock::given(...).and(method(...)) call so the matcher receives &str; apply the
same change to all mentioned mock blocks.

Comment on lines +28 to +41
#[derive(Deserialize)]
struct QueryResult {}
let result = app
.state
.mm
.db()
.await
.expect("Expected Database to be connected")
.query("SELECT id FROM ONLY subscriptions WHERE email = 'ursula_le_guin@gmail.com'")
.await
.expect("query should be successful")
.take::<Option<QueryResult>>(0)
.expect("query result should be valid");

Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue

Deserialize the SELECT result correctly.

Empty struct cannot deserialize an object with id; define id and keep Option<>.

-    #[derive(Deserialize)]
-    struct QueryResult {}
+    #[derive(Deserialize)]
+    struct QueryResult {
+        #[serde(rename = "id")]
+        id: String
+    }
📝 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
#[derive(Deserialize)]
struct QueryResult {}
let result = app
.state
.mm
.db()
.await
.expect("Expected Database to be connected")
.query("SELECT id FROM ONLY subscriptions WHERE email = 'ursula_le_guin@gmail.com'")
.await
.expect("query should be successful")
.take::<Option<QueryResult>>(0)
.expect("query result should be valid");
#[derive(Deserialize)]
struct QueryResult {
#[serde(rename = "id")]
id: String
}
🤖 Prompt for AI Agents
In tests/api/subscriptions.rs around lines 28 to 41, the test defines an empty
QueryResult struct which cannot deserialize the SELECT result that includes an
id; update the struct to include the id field with the correct type (e.g., id:
i64 or the actual DB id type) and keep the Option<QueryResult> handling so the
query can deserialize successfully; ensure the struct derives Deserialize (and
Debug if helpful).

@TN19N TN19N merged commit 017b82d into main Sep 1, 2025
2 checks passed
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Chapter 7

1 participant