Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
61ea97265c | ||
|
|
250a18cfbb |
@@ -3,3 +3,6 @@
|
|||||||
/sustenance.db*
|
/sustenance.db*
|
||||||
/.env
|
/.env
|
||||||
/seed.json
|
/seed.json
|
||||||
|
/e2e/node_modules/
|
||||||
|
/e2e/test-results/
|
||||||
|
/e2e/playwright-report/
|
||||||
|
|||||||
@@ -62,3 +62,24 @@ cargo fmt --all -- --check
|
|||||||
cargo check
|
cargo check
|
||||||
cargo test
|
cargo test
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### End-to-end tests (Playwright)
|
||||||
|
|
||||||
|
The e2e tests live in `e2e/` and use Playwright with a real browser. Each test
|
||||||
|
starts its own server against a fresh, throwaway database on a unique port, so
|
||||||
|
tests are fully isolated from each other and from your real `sustenance.db`.
|
||||||
|
|
||||||
|
```sh
|
||||||
|
# one-time setup
|
||||||
|
cd e2e
|
||||||
|
npm install
|
||||||
|
npx playwright install chromium
|
||||||
|
|
||||||
|
# run the tests (builds the server automatically via globalSetup)
|
||||||
|
cd e2e
|
||||||
|
npx playwright test
|
||||||
|
```
|
||||||
|
|
||||||
|
The Playwright `globalSetup` runs `cargo build` before the suite, and each test
|
||||||
|
launches its own server against a fresh database, so no manual build or server
|
||||||
|
start is required.
|
||||||
|
|||||||
@@ -0,0 +1,98 @@
|
|||||||
|
import { test as base, expect, Page } from "@playwright/test";
|
||||||
|
import { spawn, ChildProcess } from "child_process";
|
||||||
|
import * as fs from "fs";
|
||||||
|
import * as os from "os";
|
||||||
|
import * as path from "path";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Starts a fresh Sustenance server against a unique, throwaway database on a
|
||||||
|
* unique port for each test, and tears it down afterwards. This gives every
|
||||||
|
* test a clean DB with no shared state between tests.
|
||||||
|
*/
|
||||||
|
export const test = base.extend<{ server: { baseURL: string }; page: Page }>({
|
||||||
|
server: [
|
||||||
|
async ({}, use) => {
|
||||||
|
const dbPath = path.join(
|
||||||
|
os.tmpdir(),
|
||||||
|
`sustenance-e2e-${process.pid}-${Date.now()}-${Math.random()
|
||||||
|
.toString(36)
|
||||||
|
.slice(2)}.db`,
|
||||||
|
);
|
||||||
|
const port = 3200 + Math.floor(Math.random() * 2000);
|
||||||
|
const baseURL = `http://127.0.0.1:${port}`;
|
||||||
|
|
||||||
|
const child = spawn(
|
||||||
|
path.resolve(__dirname, "..", "target", "debug", "sustenance"),
|
||||||
|
[],
|
||||||
|
{
|
||||||
|
env: {
|
||||||
|
...process.env,
|
||||||
|
DATABASE_PATH: dbPath,
|
||||||
|
REGISTRATION_MODE: "open",
|
||||||
|
BIND_ADDRESS: `127.0.0.1:${port}`,
|
||||||
|
// Point SEED_CONFIG at a nonexistent file so no default user is created.
|
||||||
|
SEED_CONFIG: path.join(os.tmpdir(), "sustenance-e2e-no-seed.json"),
|
||||||
|
},
|
||||||
|
stdio: "ignore",
|
||||||
|
// Run in its own process group so we can kill the whole tree.
|
||||||
|
detached: true,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
await waitForServer(baseURL, child);
|
||||||
|
|
||||||
|
await use({ baseURL });
|
||||||
|
|
||||||
|
await killTree(child);
|
||||||
|
// Clean up the DB files (including -wal / -shm).
|
||||||
|
for (const suffix of ["", "-wal", "-shm"]) {
|
||||||
|
fs.rmSync(dbPath + suffix, { force: true });
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ scope: "test", auto: true },
|
||||||
|
],
|
||||||
|
|
||||||
|
// Provide a page whose baseURL points at this test's server.
|
||||||
|
page: async ({ browser, server }, use) => {
|
||||||
|
const context = await browser.newContext({ baseURL: server.baseURL });
|
||||||
|
const page = await context.newPage();
|
||||||
|
await use(page);
|
||||||
|
await context.close();
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
async function waitForServer(baseURL: string, child: ChildProcess) {
|
||||||
|
const deadline = Date.now() + 60_000;
|
||||||
|
while (Date.now() < deadline) {
|
||||||
|
if (child.exitCode !== null) {
|
||||||
|
throw new Error(`server exited early with code ${child.exitCode}`);
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const res = await fetch(baseURL + "/login");
|
||||||
|
if (res.ok) return;
|
||||||
|
} catch {
|
||||||
|
// not up yet
|
||||||
|
}
|
||||||
|
await new Promise((r) => setTimeout(r, 200));
|
||||||
|
}
|
||||||
|
throw new Error("timed out waiting for server to start");
|
||||||
|
}
|
||||||
|
|
||||||
|
async function killTree(child: ChildProcess) {
|
||||||
|
try {
|
||||||
|
process.kill(-child.pid!, "SIGTERM");
|
||||||
|
} catch {
|
||||||
|
child.kill("SIGTERM");
|
||||||
|
}
|
||||||
|
// Give it a moment to shut down gracefully, then force-kill if needed.
|
||||||
|
const exited = new Promise((resolve) => child.once("exit", resolve));
|
||||||
|
const timeout = new Promise((resolve) => setTimeout(resolve, 5000));
|
||||||
|
await Promise.race([exited, timeout]);
|
||||||
|
try {
|
||||||
|
process.kill(-child.pid!, "SIGKILL");
|
||||||
|
} catch {
|
||||||
|
/* already gone */
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export { expect };
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
import { execSync } from "child_process";
|
||||||
|
import * as path from "path";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Builds the Rust server before the test suite runs, so the fixture can launch
|
||||||
|
* `target/debug/sustenance` without requiring a manual `cargo build`.
|
||||||
|
*/
|
||||||
|
export default function globalSetup() {
|
||||||
|
execSync("cargo build", {
|
||||||
|
cwd: path.resolve(__dirname, ".."),
|
||||||
|
stdio: "inherit",
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
import { Page, expect } from "@playwright/test";
|
||||||
|
|
||||||
|
/** Registers a fresh account and lands on the lists page. */
|
||||||
|
export async function registerAndLogin(page: Page, email: string) {
|
||||||
|
await page.goto("/register");
|
||||||
|
await page.fill("#display-name", "Test User");
|
||||||
|
await page.fill("#email", email);
|
||||||
|
await page.fill("#password", "a-strong-password");
|
||||||
|
await page.click('button[type="submit"]');
|
||||||
|
await expect(page).toHaveURL(/\/lists/);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Creates a meal with the given name and markdown description. */
|
||||||
|
export async function createMeal(page: Page, name: string, description: string) {
|
||||||
|
await page.goto("/meals/new");
|
||||||
|
await page.fill("#meal-name", name);
|
||||||
|
await page.fill("#meal-description", description);
|
||||||
|
await page.click('button:has-text("Save meal")');
|
||||||
|
await expect(page).toHaveURL(/\/meals\/\d+/);
|
||||||
|
}
|
||||||
Generated
+74
@@ -0,0 +1,74 @@
|
|||||||
|
{
|
||||||
|
"name": "sustenance-e2e",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"lockfileVersion": 3,
|
||||||
|
"requires": true,
|
||||||
|
"packages": {
|
||||||
|
"": {
|
||||||
|
"name": "sustenance-e2e",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"devDependencies": {
|
||||||
|
"@playwright/test": "^1.45.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@playwright/test": {
|
||||||
|
"version": "1.62.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.62.1.tgz",
|
||||||
|
"integrity": "sha512-DTcUc8qii+cpHvtOwggMtBRMjKZHXYWdw8syRYu2vtzuq4Wxphqq4NfCs5Zt44L6mA8rfDfj+PHnxFc/FeK6mQ==",
|
||||||
|
"dev": true,
|
||||||
|
"dependencies": {
|
||||||
|
"playwright": "1.62.1"
|
||||||
|
},
|
||||||
|
"bin": {
|
||||||
|
"playwright": "cli.js"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=20"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/fsevents": {
|
||||||
|
"version": "2.3.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz",
|
||||||
|
"integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==",
|
||||||
|
"dev": true,
|
||||||
|
"hasInstallScript": true,
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"darwin"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/playwright": {
|
||||||
|
"version": "1.62.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/playwright/-/playwright-1.62.1.tgz",
|
||||||
|
"integrity": "sha512-0M+L3LAD8/nm554LOla9Ayx0j0tmFZ0FBcoQ7F1VuVHpM/XpiC8RcDzBQB8W5+hA8L22THxELzeF+2WcUzvcLg==",
|
||||||
|
"dev": true,
|
||||||
|
"dependencies": {
|
||||||
|
"playwright-core": "1.62.1"
|
||||||
|
},
|
||||||
|
"bin": {
|
||||||
|
"playwright": "cli.js"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=20"
|
||||||
|
},
|
||||||
|
"optionalDependencies": {
|
||||||
|
"fsevents": "2.3.2"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/playwright-core": {
|
||||||
|
"version": "1.62.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.62.1.tgz",
|
||||||
|
"integrity": "sha512-wPYSwEBJY9GHraISXqyqtx0na0LpO3XEX7jNDhntbex7tzUS7kLnZsOlFruFJB4Hi/rhDMjXGqHewDZ68nYZVw==",
|
||||||
|
"dev": true,
|
||||||
|
"bin": {
|
||||||
|
"playwright-core": "cli.js"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=20"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
{
|
||||||
|
"name": "sustenance-e2e",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"private": true,
|
||||||
|
"scripts": {
|
||||||
|
"test": "playwright test",
|
||||||
|
"test:headed": "playwright test --headed"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@playwright/test": "^1.45.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
import { defineConfig } from "@playwright/test";
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
testDir: "./tests",
|
||||||
|
timeout: 30_000,
|
||||||
|
retries: 0,
|
||||||
|
globalSetup: "./global-setup.ts",
|
||||||
|
use: {
|
||||||
|
trace: "on-first-retry",
|
||||||
|
},
|
||||||
|
});
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
import { expect } from "@playwright/test";
|
||||||
|
import { test } from "../fixtures";
|
||||||
|
import { registerAndLogin } from "../helpers";
|
||||||
|
|
||||||
|
test("a user can register and log in", async ({ page }) => {
|
||||||
|
await registerAndLogin(page, "alice@example.com");
|
||||||
|
await expect(page.locator("h1")).toContainText("Grocery lists");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("a user can log out", async ({ page }) => {
|
||||||
|
await registerAndLogin(page, "alice@example.com");
|
||||||
|
await page.click('button:has-text("Sign out")');
|
||||||
|
await expect(page).toHaveURL(/\/login/);
|
||||||
|
await expect(page.locator("h1")).toContainText("Welcome back");
|
||||||
|
});
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
import { expect } from "@playwright/test";
|
||||||
|
import { test } from "../fixtures";
|
||||||
|
import { registerAndLogin } from "../helpers";
|
||||||
|
|
||||||
|
test("a user can create a meal", async ({ page }) => {
|
||||||
|
await registerAndLogin(page, "alice@example.com");
|
||||||
|
|
||||||
|
await page.goto("/meals/new");
|
||||||
|
await page.fill("#meal-name", "Spaghetti Bolognese");
|
||||||
|
await page.fill("#meal-description", "## Ingredients\n\nA classic weeknight dinner.");
|
||||||
|
await page.click('button:has-text("Save meal")');
|
||||||
|
|
||||||
|
// Lands on the meal detail page and renders the markdown description.
|
||||||
|
await expect(page).toHaveURL(/\/meals\/\d+/);
|
||||||
|
await expect(page.locator("h1")).toContainText("Spaghetti Bolognese");
|
||||||
|
await expect(page.locator(".markdown h2")).toContainText("Ingredients");
|
||||||
|
|
||||||
|
// The meal appears on the meals index.
|
||||||
|
await page.goto("/meals");
|
||||||
|
await expect(page.locator(".list-card")).toContainText("Spaghetti Bolognese");
|
||||||
|
});
|
||||||
+9
-7
@@ -16,11 +16,7 @@ use axum::{
|
|||||||
use futures_util::{SinkExt, StreamExt};
|
use futures_util::{SinkExt, StreamExt};
|
||||||
use serde::{Deserialize, de::DeserializeOwned};
|
use serde::{Deserialize, de::DeserializeOwned};
|
||||||
use thiserror::Error;
|
use thiserror::Error;
|
||||||
use tower_http::{
|
use tower_http::{services::ServeDir, set_header::SetResponseHeaderLayer, trace::TraceLayer};
|
||||||
services::ServeDir,
|
|
||||||
set_header::SetResponseHeaderLayer,
|
|
||||||
trace::TraceLayer,
|
|
||||||
};
|
|
||||||
use tracing::{error, warn};
|
use tracing::{error, warn};
|
||||||
|
|
||||||
use crate::domain::{DomainError, SessionUser};
|
use crate::domain::{DomainError, SessionUser};
|
||||||
@@ -87,8 +83,14 @@ pub fn build_router(state: AppState) -> Router {
|
|||||||
.route("/meals/{meal_id}/edit", post(edit_meal))
|
.route("/meals/{meal_id}/edit", post(edit_meal))
|
||||||
.route("/meals/{meal_id}/delete", post(delete_meal))
|
.route("/meals/{meal_id}/delete", post(delete_meal))
|
||||||
.route("/meals/{meal_id}/ingredients", post(add_ingredient))
|
.route("/meals/{meal_id}/ingredients", post(add_ingredient))
|
||||||
.route("/meals/{meal_id}/ingredients/{ingredient_id}/edit", post(edit_ingredient))
|
.route(
|
||||||
.route("/meals/{meal_id}/ingredients/{ingredient_id}/delete", post(delete_ingredient))
|
"/meals/{meal_id}/ingredients/{ingredient_id}/edit",
|
||||||
|
post(edit_ingredient),
|
||||||
|
)
|
||||||
|
.route(
|
||||||
|
"/meals/{meal_id}/ingredients/{ingredient_id}/delete",
|
||||||
|
post(delete_ingredient),
|
||||||
|
)
|
||||||
.route("/lists/{list_id}/add-meal", post(add_meal_to_list))
|
.route("/lists/{list_id}/add-meal", post(add_meal_to_list))
|
||||||
.route("/lists/{list_id}/stream", get(list_stream))
|
.route("/lists/{list_id}/stream", get(list_stream))
|
||||||
.route("/invite/{token}", get(invitation_page))
|
.route("/invite/{token}", get(invitation_page))
|
||||||
|
|||||||
+2
-2
@@ -25,9 +25,9 @@ use crate::ports::{
|
|||||||
use crate::security::{Argon2PasswordHasher, RandomTokenGenerator};
|
use crate::security::{Argon2PasswordHasher, RandomTokenGenerator};
|
||||||
use crate::services::{AuthService, InvitationService, ListService, MealService, RegistrationMode};
|
use crate::services::{AuthService, InvitationService, ListService, MealService, RegistrationMode};
|
||||||
use crate::sqlite::{
|
use crate::sqlite::{
|
||||||
SqliteCategoryRepository, SqliteInvitationRepository, SqliteItemRepository,
|
SqliteCategoryRepository, SqliteDatabase, SqliteInvitationRepository, SqliteItemRepository,
|
||||||
SqliteListRepository, SqliteMealIngredientRepository, SqliteMealRepository,
|
SqliteListRepository, SqliteMealIngredientRepository, SqliteMealRepository,
|
||||||
SqliteSessionRepository, SqliteDatabase, SqliteUserRepository,
|
SqliteSessionRepository, SqliteUserRepository,
|
||||||
};
|
};
|
||||||
|
|
||||||
#[tokio::main]
|
#[tokio::main]
|
||||||
|
|||||||
+1
-10
@@ -64,16 +64,7 @@ pub trait ListRepository: Send + Sync {
|
|||||||
#[async_trait]
|
#[async_trait]
|
||||||
pub trait CategoryRepository: Send + Sync {
|
pub trait CategoryRepository: Send + Sync {
|
||||||
async fn categories(&self, txn: &mut SqliteConnection) -> DomainResult<Vec<Category>>;
|
async fn categories(&self, txn: &mut SqliteConnection) -> DomainResult<Vec<Category>>;
|
||||||
async fn create_category(
|
async fn create_category(&self, txn: &mut SqliteConnection, name: String) -> DomainResult<i64>;
|
||||||
&self,
|
|
||||||
txn: &mut SqliteConnection,
|
|
||||||
name: String,
|
|
||||||
) -> DomainResult<i64>;
|
|
||||||
async fn category_by_name(
|
|
||||||
&self,
|
|
||||||
txn: &mut SqliteConnection,
|
|
||||||
name: String,
|
|
||||||
) -> DomainResult<Option<Category>>;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A single item to insert in bulk, without a per-item revision bump.
|
/// A single item to insert in bulk, without a per-item revision bump.
|
||||||
|
|||||||
+16
-67
@@ -7,12 +7,11 @@ use sha2::{Digest, Sha256};
|
|||||||
use sqlx::{Connection, Row, SqliteConnection, SqlitePool, sqlite::SqliteConnectOptions};
|
use sqlx::{Connection, Row, SqliteConnection, SqlitePool, sqlite::SqliteConnectOptions};
|
||||||
|
|
||||||
use crate::domain::{
|
use crate::domain::{
|
||||||
Category, DomainError, DomainResult, GroceryList, Item, Meal, MealIngredient, SessionUser,
|
Category, DomainError, DomainResult, GroceryList, Item, Meal, MealIngredient, SessionUser, User,
|
||||||
User,
|
|
||||||
};
|
};
|
||||||
use crate::ports::{
|
use crate::ports::{
|
||||||
CategoryRepository, InvitationRepository, ItemRepository, ListRepository, MealIngredientRepository,
|
CategoryRepository, InvitationRepository, ItemRepository, ListRepository,
|
||||||
MealRepository, NewItem, SessionRepository, UserRepository,
|
MealIngredientRepository, MealRepository, NewItem, SessionRepository, UserRepository,
|
||||||
};
|
};
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
@@ -416,11 +415,7 @@ impl CategoryRepository for SqliteCategoryRepository {
|
|||||||
.collect())
|
.collect())
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn create_category(
|
async fn create_category(&self, txn: &mut SqliteConnection, name: String) -> DomainResult<i64> {
|
||||||
&self,
|
|
||||||
txn: &mut SqliteConnection,
|
|
||||||
name: String,
|
|
||||||
) -> DomainResult<i64> {
|
|
||||||
let position: i64 = sqlx::query("SELECT COALESCE(MAX(position), -1) + 1 FROM categories")
|
let position: i64 = sqlx::query("SELECT COALESCE(MAX(position), -1) + 1 FROM categories")
|
||||||
.fetch_one(&mut *txn)
|
.fetch_one(&mut *txn)
|
||||||
.await
|
.await
|
||||||
@@ -447,26 +442,6 @@ impl CategoryRepository for SqliteCategoryRepository {
|
|||||||
.get::<i64, _>(0);
|
.get::<i64, _>(0);
|
||||||
Ok(id)
|
Ok(id)
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn category_by_name(
|
|
||||||
&self,
|
|
||||||
txn: &mut SqliteConnection,
|
|
||||||
name: String,
|
|
||||||
) -> DomainResult<Option<Category>> {
|
|
||||||
let row = sqlx::query(
|
|
||||||
"SELECT id, name
|
|
||||||
FROM categories
|
|
||||||
WHERE name = ?1 COLLATE NOCASE",
|
|
||||||
)
|
|
||||||
.bind(&name)
|
|
||||||
.fetch_optional(&mut *txn)
|
|
||||||
.await
|
|
||||||
.map_err(db_error)?;
|
|
||||||
Ok(row.map(|row| Category {
|
|
||||||
id: row.get(0),
|
|
||||||
name: row.get(1),
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Copy)]
|
#[derive(Clone, Copy)]
|
||||||
@@ -975,10 +950,7 @@ impl MealIngredientRepository for SqliteMealIngredientRepository {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn ensure_category(
|
async fn ensure_category(txn: &mut SqliteConnection, category_id: Option<i64>) -> DomainResult<()> {
|
||||||
txn: &mut SqliteConnection,
|
|
||||||
category_id: Option<i64>,
|
|
||||||
) -> DomainResult<()> {
|
|
||||||
let Some(category_id) = category_id else {
|
let Some(category_id) = category_id else {
|
||||||
return Ok(());
|
return Ok(());
|
||||||
};
|
};
|
||||||
@@ -1381,11 +1353,7 @@ mod tests {
|
|||||||
let id = db
|
let id = db
|
||||||
.run(move |txn| {
|
.run(move |txn| {
|
||||||
let categories = categories.clone();
|
let categories = categories.clone();
|
||||||
Box::pin(async move {
|
Box::pin(async move { categories.create_category(txn, "Bakery".into()).await })
|
||||||
categories
|
|
||||||
.create_category(txn, "Bakery".into())
|
|
||||||
.await
|
|
||||||
})
|
|
||||||
})
|
})
|
||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
@@ -1402,34 +1370,12 @@ mod tests {
|
|||||||
let result = db
|
let result = db
|
||||||
.run(move |txn| {
|
.run(move |txn| {
|
||||||
let categories = categories.clone();
|
let categories = categories.clone();
|
||||||
Box::pin(async move {
|
Box::pin(async move { categories.create_category(txn, "Produce".into()).await })
|
||||||
categories
|
|
||||||
.create_category(txn, "Produce".into())
|
|
||||||
.await
|
|
||||||
})
|
|
||||||
})
|
})
|
||||||
.await;
|
.await;
|
||||||
assert!(matches!(result, Err(DomainError::Conflict)));
|
assert!(matches!(result, Err(DomainError::Conflict)));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
|
||||||
async fn category_by_name_resolves_globally() {
|
|
||||||
let db = setup().await;
|
|
||||||
let categories = SqliteCategoryRepository;
|
|
||||||
let found = db
|
|
||||||
.run(move |txn| {
|
|
||||||
let categories = categories.clone();
|
|
||||||
Box::pin(async move {
|
|
||||||
categories
|
|
||||||
.category_by_name(txn, "produce".into())
|
|
||||||
.await
|
|
||||||
})
|
|
||||||
})
|
|
||||||
.await
|
|
||||||
.unwrap();
|
|
||||||
assert_eq!(found.unwrap().name, "Produce");
|
|
||||||
}
|
|
||||||
|
|
||||||
// ---- ItemRepository ----
|
// ---- ItemRepository ----
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
@@ -1977,7 +1923,14 @@ mod tests {
|
|||||||
let ingredients = ingredients.clone();
|
let ingredients = ingredients.clone();
|
||||||
Box::pin(async move {
|
Box::pin(async move {
|
||||||
ingredients
|
ingredients
|
||||||
.add_ingredient(txn, meal.id, "X".into(), String::new(), String::new(), Some(9999))
|
.add_ingredient(
|
||||||
|
txn,
|
||||||
|
meal.id,
|
||||||
|
"X".into(),
|
||||||
|
String::new(),
|
||||||
|
String::new(),
|
||||||
|
Some(9999),
|
||||||
|
)
|
||||||
.await
|
.await
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
@@ -2054,11 +2007,7 @@ mod tests {
|
|||||||
let result = db
|
let result = db
|
||||||
.run(move |txn| {
|
.run(move |txn| {
|
||||||
let ingredients = ingredients.clone();
|
let ingredients = ingredients.clone();
|
||||||
Box::pin(async move {
|
Box::pin(async move { ingredients.delete_ingredient(txn, meal.id, 9999).await })
|
||||||
ingredients
|
|
||||||
.delete_ingredient(txn, meal.id, 9999)
|
|
||||||
.await
|
|
||||||
})
|
|
||||||
})
|
})
|
||||||
.await;
|
.await;
|
||||||
assert!(matches!(result, Err(DomainError::NotFound)));
|
assert!(matches!(result, Err(DomainError::NotFound)));
|
||||||
|
|||||||
Reference in New Issue
Block a user