Tests refactor
This commit is contained in:
parent
245520fb50
commit
99134d8556
165 changed files with 911 additions and 531 deletions
|
|
@ -46,21 +46,24 @@ docker compose -f docker/docker-compose.yml up -d # Start all services
|
||||||
docker compose -f docker/docker-compose.yml down # Stop all services
|
docker compose -f docker/docker-compose.yml down # Stop all services
|
||||||
```
|
```
|
||||||
|
|
||||||
## Implementation Workflow
|
## Implementation Workflow (TDD-First)
|
||||||
|
|
||||||
Every feature or phase implementation MUST start with a rigorous planning and design phase:
|
Every feature or phase implementation MUST start with a rigorous planning and design phase:
|
||||||
|
|
||||||
0. **Mandatory Planning (Grilling)**: Before starting any feature or bug fix, you MUST invoke the `grill-with-docs` skill.
|
0. **Mandatory Planning (Grilling)**: Before starting any feature or bug fix, you MUST invoke the `grill-with-docs` skill.
|
||||||
- Run the skill using: `view_file` on `.agents/skills/grill-with-docs/SKILL.md` and follow its instructions.
|
- Run the skill using: `view_file` on `.agents/skills/grill-with-docs/SKILL.md` and follow its instructions.
|
||||||
- This session will stress-test your plan against the existing domain model, terminology, and documentation (`CONTEXT.md`, ADRs).
|
- Stress-test your plan against the existing domain model, terminology, and documentation (`CONTEXT.md`, ADRs, `docs/tdd-transition-plan.md`).
|
||||||
- Decisions must be crystallized and documentation (glossary/ADRs) updated before moving to implementation.
|
- Decisions must be crystallized and documentation updated before moving to implementation.
|
||||||
|
|
||||||
1. **Vertical Slice Implementation**: Follow this approach for the actual build:
|
1. **Strict TDD Cycle**: All code changes must strictly follow the Red-Green-Refactor loop as detailed in [TDD Guide](docs/instructions/tdd.md). Do not write any production code before writing its failing test.
|
||||||
2. **Database Layer**: Create the Mongoose schema and Repository in `packages/api`. All reads must use `.lean().exec()`.
|
|
||||||
3. **Service Layer**: Implement business logic in the Service class, using Awilix for constructor injection. Add unit tests.
|
2. **Vertical Slice TDD Order**: Build each vertical slice following this exact test-first sequence:
|
||||||
4. **Route Layer**: Create the Fastify route plugin and register it. Add route tests (using `app.inject`).
|
- **Shared Layer**: Write validation schema tests first in `packages/shared/tests/` -> Implement schemas in `packages/shared/src/`.
|
||||||
5. **Web API Client**: Implement the frontend service in `packages/web/src/services/`. Add unit tests.
|
- **Database Layer**: Write integration tests in `packages/api/tests/` using `mongodb-memory-server` -> Implement Mongoose schemas and Repository in `packages/api/src/`. All reads must use `.lean().exec()`.
|
||||||
6. **Web UI**: Create the Next.js pages and components. Use Server Components by default. Add component tests (React Testing Library).
|
- **Service Layer**: Write unit tests in `packages/api/tests/` using `createMockRepository` -> Implement business logic Service class in `packages/api/src/` with Awilix constructor injection.
|
||||||
|
- **Route Layer**: Write route tests in `packages/api/tests/` using `app.inject` -> Implement Fastify route plugin in `packages/api/src/`.
|
||||||
|
- **Web API Client**: Write service unit tests in `packages/web/tests/` using MSW mocking -> Implement the service client in `packages/web/src/`.
|
||||||
|
- **Web UI**: Write component tests in `packages/web/tests/` using React Testing Library -> Implement Next.js pages and components in `packages/web/src/`. Use Server Components by default.
|
||||||
|
|
||||||
**Verification Gate (Mandatory)**:
|
**Verification Gate (Mandatory)**:
|
||||||
Before considering a task complete, run:
|
Before considering a task complete, run:
|
||||||
|
|
|
||||||
|
|
@ -97,19 +97,20 @@ Separate each group with a blank line.
|
||||||
| React hook | camelCase | `useProducts`, `usePantryItems` |
|
| React hook | camelCase | `useProducts`, `usePantryItems` |
|
||||||
| CSS class (Tailwind) | kebab-case | via Tailwind utilities |
|
| CSS class (Tailwind) | kebab-case | via Tailwind utilities |
|
||||||
|
|
||||||
## Implementation Workflow (Vertical Slice)
|
## Implementation Workflow (TDD-First)
|
||||||
|
|
||||||
Every feature or phase implementation must follow this order to ensure consistency and type safety across the monorepo:
|
Every feature or phase implementation must follow this exact test-first sequence, strictly practicing the Red-Green-Refactor cycle:
|
||||||
|
|
||||||
1. **Shared Layer (`packages/shared`)**: Define types, enums, and Zod schemas. Add unit tests.
|
1. **Shared Layer (`packages/shared`)**: Write validation schema and enum tests first in `tests/`. Implement schemas and types inside `src/`.
|
||||||
2. **Database Layer (`packages/api`)**: Create Mongoose schema and Repository. Use `.lean().exec()` on all reads. Add integration tests.
|
2. **Database Layer (`packages/api`)**: Write schema and repository integration tests first in `tests/` (using `mongodb-memory-server`). Create Mongoose schema and Repository in `src/`. All reads must use `.lean().exec()`.
|
||||||
3. **Service Layer (`packages/api`)**: Implement business logic using Awilix DI. Add unit tests.
|
3. **Service Layer (`packages/api`)**: Write service unit tests first in `tests/` using the `createMockRepository` helper to mock repository methods. Implement business logic Service class inside `src/` using Awilix constructor injection.
|
||||||
4. **Route Layer (`packages/api`)**: Create Fastify route plugin. Add route tests.
|
4. **Route Layer (`packages/api`)**: Write Fastify route plugin tests first in `tests/` using `app.inject` and resolving mocks. Implement the Fastify route plugin inside `src/`.
|
||||||
5. **Web API Client (`packages/web`)**: Implement frontend service. Add unit tests.
|
5. **Web API Client (`packages/web`)**: Write service unit tests first in `tests/` utilizing MSW to mock backend requests. Implement frontend service in `src/`.
|
||||||
6. **Web UI (`packages/web`)**: Create Next.js pages/components (Server Components by default). Add component tests.
|
6. **Web UI (`packages/web`)**: Write component and page tests first in `tests/` using React Testing Library role queries. Create Next.js pages and components inside `src/` (Server Components by default).
|
||||||
|
|
||||||
**Mandatory Verification**: Every task must end with `npm run build`, `npm run test:cov`, and `npm run lint` all passing with the defined coverage thresholds.
|
**Mandatory Verification**: Every task must end with `npm run build`, `npm run test:cov`, and `npm run lint` all passing with the defined coverage thresholds.
|
||||||
|
|
||||||
|
|
||||||
## Code Organization Rules
|
## Code Organization Rules
|
||||||
|
|
||||||
### API (NestJS)
|
### API (NestJS)
|
||||||
|
|
|
||||||
167
docs/instructions/tdd.md
Normal file
167
docs/instructions/tdd.md
Normal file
|
|
@ -0,0 +1,167 @@
|
||||||
|
# Test-Driven Development (TDD) Guide — MeshiTrack
|
||||||
|
|
||||||
|
This guide serves as the definitive reference for writing, executing, and maintaining tests using a Test-Driven Development (TDD) methodology across all packages in the MeshiTrack monorepo.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. Core Principles
|
||||||
|
|
||||||
|
TDD is a software development workflow where you **write tests first, before writing implementation code**. This ensures high reliability, enforces strict scoping of features, prevents untested paths, and results in cleaner designs.
|
||||||
|
|
||||||
|
### The Red-Green-Refactor Cycle
|
||||||
|
|
||||||
|
For every feature, schema, service, or route you build, follow this continuous loop:
|
||||||
|
|
||||||
|
1. **RED (Write a Failing Test)**:
|
||||||
|
- Identify the small increment of behavior you want to add.
|
||||||
|
- Write a test expressing that behavior.
|
||||||
|
- Run the test and see it fail. **Compilation failures in TypeScript count as a valid "Red" state** for new types, interfaces, or class signatures. For logic, stub the signature and ensure it fails at runtime.
|
||||||
|
2. **GREEN (Write Minimal Code)**:
|
||||||
|
- Write the absolute minimum production code required to satisfy the test (even if it's returning hardcoded values temporarily).
|
||||||
|
- Run the test and see it pass.
|
||||||
|
3. **REFACTOR (Clean Up and Optimize)**:
|
||||||
|
- Review your code (both production and test files). Remove duplication, improve readability, refine types, and optimize structure.
|
||||||
|
- Run the test suite continuously to guarantee your refactoring did not break existing behavior (remains Green).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. Directory Structure (Option B)
|
||||||
|
|
||||||
|
To keep production bundles completely clean, all test files and mock utilities live in a root-level `tests/` folder matching the structure of `src/`. **Never place tests inside `src/`.**
|
||||||
|
|
||||||
|
```
|
||||||
|
packages/api/
|
||||||
|
├── src/ # 100% Pure Production Code
|
||||||
|
│ ├── modules/
|
||||||
|
│ │ └── products/
|
||||||
|
│ │ ├── products.service.ts
|
||||||
|
│ │ └── products.repository.ts
|
||||||
|
│ └── main.ts
|
||||||
|
└── tests/ # 100% Test-First Files
|
||||||
|
├── helpers/
|
||||||
|
│ └── mock-repository.ts # Reusable mocking helpers
|
||||||
|
├── modules/
|
||||||
|
│ └── products/
|
||||||
|
│ ├── products.service.test.ts
|
||||||
|
│ └── products.repository.test.ts
|
||||||
|
└── schemas/
|
||||||
|
└── product.schema.test.ts
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. Layer-by-Layer TDD Guide
|
||||||
|
|
||||||
|
### Layer 1: Shared Package (`packages/shared`)
|
||||||
|
The shared package contains domain types, enums, and Zod schemas.
|
||||||
|
|
||||||
|
1. **Red**: Write a test in `packages/shared/tests/validation/feature.schemas.test.ts` asserting validation behaviors for correct, incorrect, and edge-case payloads.
|
||||||
|
2. **Green**: Implement the validation schema and types in `packages/shared/src/validation/feature.schemas.ts`. Run `npm run test -w packages/shared` to verify it passes.
|
||||||
|
3. **Refactor**: Clean up custom messages, dry up repeated Zod rules, and ensure exports are barrelled correctly in `index.ts`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Layer 2: Schema & Repository (`packages/api`)
|
||||||
|
This layer handles persistence via Mongoose schemas and repositories.
|
||||||
|
|
||||||
|
1. **Red**: Write an integration test in `packages/api/tests/modules/<module>/<module>.repository.test.ts` using `mongodb-memory-server` asserting query conditions, indexes, and `householdId` filtering.
|
||||||
|
2. **Green**: Create the Mongoose schema inside `src/schemas/` and the Repository class in `src/modules/<module>/`. All reads must use `.lean().exec()`. Verify tests pass.
|
||||||
|
3. **Refactor**: Optimize indexes and verify schema options (e.g. `timestamps: true`).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Layer 3: Service Layer (`packages/api`)
|
||||||
|
Services contain core business logic, transactional bounds, and use constructor-injected dependencies via Awilix.
|
||||||
|
|
||||||
|
To eliminate boilerplate, import the `createMockRepository` helper from `tests/helpers/mock-repository.ts` to mock all repository methods automatically:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// packages/api/tests/modules/products/products.service.test.ts
|
||||||
|
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||||
|
import { ProductsService } from '../../../src/modules/products/products.service.js';
|
||||||
|
import { ProductsRepository } from '../../../src/modules/products/products.repository.js';
|
||||||
|
import { createMockRepository } from '../../helpers/mock-repository.js';
|
||||||
|
|
||||||
|
describe(ProductsService.name, () => {
|
||||||
|
let service: ProductsService;
|
||||||
|
let mockRepo: ReturnType<typeof createMockRepository<ProductsRepository>>;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
vi.clearAllMocks();
|
||||||
|
mockRepo = createMockRepository(ProductsRepository);
|
||||||
|
service = new ProductsService({ productsRepository: mockRepo as any });
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should find a product by household scope', async () => {
|
||||||
|
// 1. RED: Write the test first, before service.findById exists
|
||||||
|
mockRepo.findById.mockResolvedValue({ _id: '1', name: 'Apples' });
|
||||||
|
const result = await service.getById('1', 'hh1');
|
||||||
|
expect(result.name).toBe('Apples');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
1. **Red**: Run the test. Verify it fails due to compilation (if the service/method is missing) or runtime failure (if stubbed).
|
||||||
|
2. **Green**: Implement constructor injection and the method in `src/modules/<module>/<module>.service.ts`. Run the test to confirm it passes.
|
||||||
|
3. **Refactor**: Extract logic, reduce cognitive complexity, and ensure proper typing.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Layer 4: Route Layer (`packages/api`)
|
||||||
|
Fastify routes define endpoints, validate payloads, and resolve services.
|
||||||
|
|
||||||
|
1. **Red**: Write a route test in `tests/modules/<module>/<module>.routes.test.ts` using Fastify's `app.inject()`. Mock the resolved service from the request DI scope. Assert status codes and payloads.
|
||||||
|
2. **Green**: Register the route plugin in `src/modules/<module>/<module>.routes.ts`, wire validations via Zod type providers, and call the service. Verify it passes.
|
||||||
|
3. **Refactor**: Clean up endpoint structures, route prefixes, and schema schemas.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Layer 5: Web API Client (`packages/web`)
|
||||||
|
Frontend fetch wrapper utilizing MSW to mock backend requests.
|
||||||
|
|
||||||
|
1. **Red**: Write a service test in `packages/web/tests/services/<service>.test.ts`. Configure MSW to mock the HTTP responses. Assert response modeling and mapping.
|
||||||
|
2. **Green**: Implement the service wrapper using fetch in `packages/web/src/services/`. Verify it passes.
|
||||||
|
3. **Refactor**: Clean up payload mappings and parameter serialization.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Layer 6: Web UI Components (`packages/web`)
|
||||||
|
React components and Next.js pages.
|
||||||
|
|
||||||
|
1. **Red**: Write a component test in `packages/web/tests/app/(dashboard)/<feature>/<Component>.test.tsx` using React Testing Library. Use accessible role queries (`screen.getByRole`) to assert loading/rendering states and user interactions.
|
||||||
|
2. **Green**: Write the minimal TSX/JSX inside `packages/web/src/` to satisfy the tests.
|
||||||
|
3. **Refactor**: Refine tailwind utility classes, modularize components, and check accessibility (ARIA tags).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. Tooling & DX Guidelines
|
||||||
|
|
||||||
|
To make TDD fast and effective, use interactive watch-mode running continuously in the background. **Never run `npx`.**
|
||||||
|
|
||||||
|
### Continuous File-Specific Watch Mode (Fastest DX)
|
||||||
|
To start watch mode for a specific test file during a Red-Green cycle:
|
||||||
|
```bash
|
||||||
|
# Watch API Service tests
|
||||||
|
npm run test:watch -w packages/api -- tests/modules/products/products.service.test.ts
|
||||||
|
|
||||||
|
# Watch Web Component tests
|
||||||
|
npm run test:watch -w packages/web -- tests/app/dashboard/ProductCard.test.tsx
|
||||||
|
```
|
||||||
|
|
||||||
|
### Continuous Package-Wide Watch Mode
|
||||||
|
To start watch mode for all tests in a package:
|
||||||
|
```bash
|
||||||
|
# Continuous API package tests
|
||||||
|
npm run test:watch -w packages/api
|
||||||
|
|
||||||
|
# Continuous Web package tests
|
||||||
|
npm run test:watch -w packages/web
|
||||||
|
```
|
||||||
|
|
||||||
|
### Full-Suite Verification
|
||||||
|
Before concluding any implementation phase, confirm that the entire verification gate is fully operational:
|
||||||
|
```bash
|
||||||
|
npm run build # Enforces typescript compile across all packages
|
||||||
|
npm run test:cov # Verifies all tests pass and meet strict coverage gates
|
||||||
|
npm run lint # Validates static conventions
|
||||||
|
```
|
||||||
181
docs/tdd-transition-plan.md
Normal file
181
docs/tdd-transition-plan.md
Normal file
|
|
@ -0,0 +1,181 @@
|
||||||
|
# TDD Transition Plan — MeshiTrack
|
||||||
|
|
||||||
|
This document details the blueprint for transitioning the MeshiTrack monorepo to a strict Test-Driven Development (TDD) workflow. It outlines the specific steps, tooling enhancements, repository guidelines, and architectural patterns required to shift from "test-after" to "test-first" development.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. Objectives
|
||||||
|
|
||||||
|
1. **Maintain Strict Quality Bars**: Ensure all new code seamlessly meets our high coverage gates (100% lines/functions/statements, 90% branches for `packages/api` & `packages/shared`, and 90% lines, 85% functions, 75% branches, 85% statements for `packages/web`).
|
||||||
|
2. **Prevent Architecture Regression**: Stop implementing business logic before definitions and type interfaces are established in `packages/shared`.
|
||||||
|
3. **Elevate DX and Iteration Speed**: Speed up the local feedback loop (Vitest/RTL runtimes) to enable a continuous Red-Green-Refactor cycle.
|
||||||
|
4. **Standardize Mocks and Factories**: Establish reusable test builders and automated dependency mocks to reduce boilerplate.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. The MeshiTrack TDD Loop (Red-Green-Refactor)
|
||||||
|
|
||||||
|
TDD requires writing tests *before* writing the corresponding implementation. In MeshiTrack, this cycle is applied iteratively across each vertical slice.
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
graph TD
|
||||||
|
A[1. Write Failing Test - RED] --> B[2. Run Test & Verify Failure]
|
||||||
|
B --> C[3. Write Minimal Code - GREEN]
|
||||||
|
C --> D[4. Run Test & Verify Pass]
|
||||||
|
D --> E[5. Refactor Code & Clean Boilerplate]
|
||||||
|
E --> F[6. Run All Tests to Prevent Regression]
|
||||||
|
F --> A
|
||||||
|
```
|
||||||
|
|
||||||
|
### The Rules of TDD in MeshiTrack:
|
||||||
|
1. **Write no production code** unless it is to make a failing unit or integration test pass.
|
||||||
|
2. **Write only enough of a test** to demonstrate a failure (compilation failure counts as a failure).
|
||||||
|
3. **Write only enough production code** to make the single failing test pass.
|
||||||
|
4. **Refactor immediately** after going Green, while both the unit test and existing regression suite are passing.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. Layer-by-Layer TDD Guide
|
||||||
|
|
||||||
|
### Layer 1: Shared Package (`packages/shared`)
|
||||||
|
The shared package contains domain types, enums, and Zod schemas. It must remain pure TypeScript.
|
||||||
|
|
||||||
|
1. **Red**: Define the interface or enum signature in TypeScript. Write a test in `src/validation/*.test.ts` asserting how the validation schema should handle valid, invalid, and edge-case payloads.
|
||||||
|
2. **Green**: Implement the Zod v4 validation schema in `src/validation/*.schemas.ts`. Run the test to verify it passes.
|
||||||
|
3. **Refactor**: Clean up the schema declarations, refine custom error messages, and ensure barrel exports are updated in `index.ts`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Layer 2: Database Schema & Repository (`packages/api`)
|
||||||
|
This layer handles persistence via Mongoose schemas and Awilix-registered repositories.
|
||||||
|
|
||||||
|
1. **Red**: Write an integration test (`*.repository.test.ts`) using `mongodb-memory-server` that tests data storage, constraints, index behavior, and `householdId` filtering.
|
||||||
|
2. **Green**: Implement the Mongoose schema, compile the model, and write the Repository class methods using `.lean().exec()` on all reads. Verify the integration test passes.
|
||||||
|
3. **Refactor**: Optimize database indexes, ensure appropriate Mongoose options are set, and check for memory leaks or unclosed connections.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Layer 3: Service Layer (`packages/api`)
|
||||||
|
Services contain the core business logic, including validation, authorization checks, and transactional tasks.
|
||||||
|
|
||||||
|
1. **Red**: Write unit tests (`*.service.test.ts`) using Vitest. Mock all dependencies (e.g., repositories, event emitters) using `vi.fn()`. Write tests asserting correct handling of successful cases, expected errors (`NotFoundError`, `ConflictError`, etc.), and household isolation boundaries.
|
||||||
|
2. **Green**: Create the Service class with constructor injection via Awilix. Implement only the minimum logic required to pass the test cases.
|
||||||
|
3. **Refactor**: Extract duplicate logic into helper functions, improve type safety, and simplify constructor dependencies.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Layer 4: Route Layer (`packages/api`)
|
||||||
|
Fastify routes define endpoints, validate incoming payloads via Zod, resolve services, and return responses.
|
||||||
|
|
||||||
|
1. **Red**: Write a route test (`*.routes.test.ts`) using Fastify's `app.inject()`. Assert status codes, headers, and the response body structure. Mock the service layer resolved from the request DI scope (`request.diScope.resolve`).
|
||||||
|
2. **Green**: Implement the Fastify route plugin, declare path validation schemas, resolve the service from the DI container, and call it. Register the plugin.
|
||||||
|
3. **Refactor**: Optimize error mappings, clean up route paths, verify route configuration options (e.g., household scope exclusions).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Layer 5: Web API Client (`packages/web`)
|
||||||
|
The client wrapper calls the backend API and handles client-side caching or state synchronization (e.g., via SWR).
|
||||||
|
|
||||||
|
1. **Red**: Write a service unit test (`*.test.ts`) using Vitest. Configure MSW (Mock Service Worker) to mock the API endpoint response. Write assertions checking payload formatting, query serialization, and error transformations.
|
||||||
|
2. **Green**: Implement the frontend service in `src/services/` using fetch. Run tests and verify MSW handlers return correctly.
|
||||||
|
3. **Refactor**: Standardize request configuration, dry up common header settings, and enhance client type-safety.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Layer 6: Web UI (`packages/web`)
|
||||||
|
React components and Next.js pages display data, capture input, and handle interactions.
|
||||||
|
|
||||||
|
1. **Red**: Write component tests (`*.test.tsx`) using React Testing Library. Query elements by accessible roles (e.g., `screen.getByRole('button', { name: /save/i })`). Assert correct rendering of states (loading, empty, success) and verify user interactions fire expected service callbacks.
|
||||||
|
2. **Green**: Implement the React component (Server/Client split as appropriate). Write the bare minimum JSX/TSX to satisfy the test roles and events.
|
||||||
|
3. **Refactor**: Refine CSS structures, optimize components for re-renders, clean up accessibility attributes, and verify responsive design parameters.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. DX Tooling Enhancements (Bridges)
|
||||||
|
|
||||||
|
To enable developers to practice TDD seamlessly, we must upgrade our test execution loop and reduce boilerplate.
|
||||||
|
|
||||||
|
### Bridge A: Immediate Watch Mode for Active Files
|
||||||
|
Running `npm run test` or `npm run test:cov` across the entire workspace takes too long for the continuous TDD loop. We need scripts to instantly watch specific directories or single files.
|
||||||
|
|
||||||
|
We will update package-level `package.json` scripts to introduce:
|
||||||
|
1. **Root package.json**:
|
||||||
|
- Add `"test:watch": "turbo run test:watch"`
|
||||||
|
2. **packages/web/package.json**:
|
||||||
|
- Add `"test:watch": "vitest"` (mirroring packages/api)
|
||||||
|
|
||||||
|
This will allow developers to run:
|
||||||
|
* `npm run test:watch -w packages/api` to start a continuous, interactive Vitest runner for API files.
|
||||||
|
* `npm run test:watch -w packages/api -- src/modules/products/products.service.test.ts` to focus only on a single service during the Red-Green cycle.
|
||||||
|
|
||||||
|
### Bridge B: Automated Mock Helpers
|
||||||
|
Creating manual mocks for every repository in every service test adds substantial boilerplate. We will introduce a standard mock utility `packages/api/src/common/test/mock-repository.ts` that dynamically mocks any repository interface.
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { vi } from 'vitest';
|
||||||
|
|
||||||
|
export function createMockRepository<T>(repoClass: new (...args: any[]) => T): Record<keyof T, any> {
|
||||||
|
const methods = Object.getOwnPropertyNames(repoClass.prototype).filter(
|
||||||
|
(name) => name !== 'constructor' && typeof repoClass.prototype[name] === 'function'
|
||||||
|
);
|
||||||
|
|
||||||
|
const mock: Record<string, any> = {};
|
||||||
|
for (const method of methods) {
|
||||||
|
mock[method] = vi.fn();
|
||||||
|
}
|
||||||
|
return mock as Record<keyof T, any>;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
This reduces the boilerplate in service tests from a manual 15-line structure to a single line:
|
||||||
|
```typescript
|
||||||
|
const mockRepo = createMockRepository(ProductsRepository);
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. TDD Commit & Git Conventions
|
||||||
|
|
||||||
|
To document and encourage step-by-step TDD, we will adopt atomic commit structures matching the Red-Green-Refactor steps:
|
||||||
|
|
||||||
|
1. **Red Commit**: When a new test suite is created and fails.
|
||||||
|
- `test(pantry): add failing test for spoilage calculation (RED)`
|
||||||
|
2. **Green Commit**: When the production code is completed and tests pass.
|
||||||
|
- `feat(pantry): implement spoilage calculation logic (GREEN)`
|
||||||
|
3. **Refactor Commit**: When the code is cleaned up while staying green.
|
||||||
|
- `refactor(pantry): optimize date difference utility in spoilage`
|
||||||
|
|
||||||
|
These atomic commits make peer reviews and rollbacks extremely clean and make it simple to track development momentum.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. Guidelines Alignment & Updates
|
||||||
|
|
||||||
|
To formalize the transition, we will execute the following file updates:
|
||||||
|
|
||||||
|
1. **Update `ANTIGRAVITY.md`**:
|
||||||
|
- Modify the "Implementation Workflow" section to make TDD mandatory.
|
||||||
|
- Replace steps with the Red-Green-Refactor workflow per layer.
|
||||||
|
- Introduce the `test:watch` commands and mock helper directives.
|
||||||
|
2. **Update `docs/instructions/conventions.md`**:
|
||||||
|
- Re-orient the "Implementation Workflow" from "test-after" to TDD-first.
|
||||||
|
3. **Introduce `docs/instructions/tdd.md`**:
|
||||||
|
- A dedicated developer tutorial on TDD in the MeshiTrack codebase, explaining mocks, databases, and Next.js testing patterns in detail.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 7. Immediate Action Plan
|
||||||
|
|
||||||
|
To bridge the TDD gap immediately, we will execute these concrete steps in this task:
|
||||||
|
|
||||||
|
1. **Step 1: Codebase Tooling Infrastructure**
|
||||||
|
- Add `"test:watch": "vitest"` to `packages/web/package.json`.
|
||||||
|
- Add `"test:watch": "turbo run test:watch"` to root `package.json`.
|
||||||
|
2. **Step 2: Reusable Mock Helper**
|
||||||
|
- Create `packages/api/src/common/test/mock-repository.ts` containing the automated mock builder utility.
|
||||||
|
3. **Step 3: Document TDD & Update Guidelines**
|
||||||
|
- Create `docs/instructions/tdd.md` as the definitive guide.
|
||||||
|
- Update `ANTIGRAVITY.md`'s workflow section.
|
||||||
|
- Update `docs/instructions/conventions.md`'s workflow section.
|
||||||
|
4. **Step 4: Verification**
|
||||||
|
- Verify the codebase build and tests are completely operational.
|
||||||
|
|
@ -12,6 +12,7 @@
|
||||||
"lint": "turbo run lint",
|
"lint": "turbo run lint",
|
||||||
"lint-fix": "turbo run lint-fix",
|
"lint-fix": "turbo run lint-fix",
|
||||||
"test": "turbo run test",
|
"test": "turbo run test",
|
||||||
|
"test:watch": "turbo run test:watch",
|
||||||
"test:cov": "turbo run test:cov",
|
"test:cov": "turbo run test:cov",
|
||||||
"typecheck": "turbo run typecheck",
|
"typecheck": "turbo run typecheck",
|
||||||
"clean": "turbo run clean",
|
"clean": "turbo run clean",
|
||||||
|
|
|
||||||
|
|
@ -133,7 +133,9 @@ export default fp(
|
||||||
// Make sure other module deps are available for classes instantiated by SuggestionEngine/ShoppingGap
|
// Make sure other module deps are available for classes instantiated by SuggestionEngine/ShoppingGap
|
||||||
recipesRepository: asClass(RecipesRepository, { lifetime: Lifetime.SINGLETON }),
|
recipesRepository: asClass(RecipesRepository, { lifetime: Lifetime.SINGLETON }),
|
||||||
pantryRepository: asClass(PantryRepository, { lifetime: Lifetime.SINGLETON }),
|
pantryRepository: asClass(PantryRepository, { lifetime: Lifetime.SINGLETON }),
|
||||||
nutritionTargetRepository: asClass(NutritionTargetRepository, { lifetime: Lifetime.SINGLETON }),
|
nutritionTargetRepository: asClass(NutritionTargetRepository, {
|
||||||
|
lifetime: Lifetime.SINGLETON,
|
||||||
|
}),
|
||||||
productsRepository: asClass(ProductsRepository, { lifetime: Lifetime.SINGLETON }),
|
productsRepository: asClass(ProductsRepository, { lifetime: Lifetime.SINGLETON }),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -181,7 +183,7 @@ export default fp(
|
||||||
variety: z.number(),
|
variety: z.number(),
|
||||||
}),
|
}),
|
||||||
reasoning: z.array(z.string()),
|
reasoning: z.array(z.string()),
|
||||||
})
|
}),
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
@ -190,7 +192,7 @@ export default fp(
|
||||||
const suggestions = await engine.getSuggestions(
|
const suggestions = await engine.getSuggestions(
|
||||||
request.params.householdId,
|
request.params.householdId,
|
||||||
request.user.keycloakId,
|
request.user.keycloakId,
|
||||||
{ limit: request.query.limit }
|
{ limit: request.query.limit },
|
||||||
);
|
);
|
||||||
return reply.send(suggestions);
|
return reply.send(suggestions);
|
||||||
},
|
},
|
||||||
|
|
@ -215,7 +217,7 @@ export default fp(
|
||||||
const service = fastify.diContainer.resolve('mealPlanService');
|
const service = fastify.diContainer.resolve('mealPlanService');
|
||||||
const plan = await service.getByWeek(
|
const plan = await service.getByWeek(
|
||||||
request.params.householdId,
|
request.params.householdId,
|
||||||
request.params.weekStartDate
|
request.params.weekStartDate,
|
||||||
);
|
);
|
||||||
if (!plan) {
|
if (!plan) {
|
||||||
return reply.status(200).send({ message: 'No meal plan scheduled for this week' });
|
return reply.status(200).send({ message: 'No meal plan scheduled for this week' });
|
||||||
|
|
@ -253,7 +255,7 @@ export default fp(
|
||||||
const plan = await service.create(
|
const plan = await service.create(
|
||||||
request.params.householdId,
|
request.params.householdId,
|
||||||
request.user.keycloakId,
|
request.user.keycloakId,
|
||||||
request.body
|
request.body,
|
||||||
);
|
);
|
||||||
return reply.status(201).send(toPlanResponse(plan as AnyPlanDoc));
|
return reply.status(201).send(toPlanResponse(plan as AnyPlanDoc));
|
||||||
},
|
},
|
||||||
|
|
@ -273,7 +275,7 @@ export default fp(
|
||||||
const plan = await service.update(
|
const plan = await service.update(
|
||||||
request.params.id,
|
request.params.id,
|
||||||
request.params.householdId,
|
request.params.householdId,
|
||||||
request.body
|
request.body,
|
||||||
);
|
);
|
||||||
return reply.send(toPlanResponse(plan as AnyPlanDoc));
|
return reply.send(toPlanResponse(plan as AnyPlanDoc));
|
||||||
},
|
},
|
||||||
|
|
@ -295,7 +297,7 @@ export default fp(
|
||||||
const plan = await service.updateStatus(
|
const plan = await service.updateStatus(
|
||||||
request.params.id,
|
request.params.id,
|
||||||
request.params.householdId,
|
request.params.householdId,
|
||||||
request.body.status
|
request.body.status,
|
||||||
);
|
);
|
||||||
return reply.send(toPlanResponse(plan as AnyPlanDoc));
|
return reply.send(toPlanResponse(plan as AnyPlanDoc));
|
||||||
},
|
},
|
||||||
|
|
@ -334,17 +336,14 @@ export default fp(
|
||||||
pantryQuantity: z.number(),
|
pantryQuantity: z.number(),
|
||||||
missingQuantity: z.number(),
|
missingQuantity: z.number(),
|
||||||
unit: z.string(),
|
unit: z.string(),
|
||||||
})
|
}),
|
||||||
),
|
),
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
handler: async (request, reply) => {
|
handler: async (request, reply) => {
|
||||||
const service = fastify.diContainer.resolve('shoppingGapService');
|
const service = fastify.diContainer.resolve('shoppingGapService');
|
||||||
const result = await service.calculateGap(
|
const result = await service.calculateGap(request.params.householdId, request.params.id);
|
||||||
request.params.householdId,
|
|
||||||
request.params.id
|
|
||||||
);
|
|
||||||
return reply.send(result);
|
return reply.send(result);
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
@ -352,5 +351,5 @@ export default fp(
|
||||||
{
|
{
|
||||||
name: 'meal-plans-routes',
|
name: 'meal-plans-routes',
|
||||||
dependencies: ['auth-plugin'],
|
dependencies: ['auth-plugin'],
|
||||||
}
|
},
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,7 @@ import type {
|
||||||
NutritionInfo,
|
NutritionInfo,
|
||||||
MealPlanDaySchema,
|
MealPlanDaySchema,
|
||||||
} from '@meshitrack/shared';
|
} from '@meshitrack/shared';
|
||||||
import { z } from 'zod/v4';
|
import { type z } from 'zod/v4';
|
||||||
|
|
||||||
type MealPlanDay = z.infer<typeof MealPlanDaySchema>;
|
type MealPlanDay = z.infer<typeof MealPlanDaySchema>;
|
||||||
|
|
||||||
|
|
@ -39,19 +39,12 @@ export class MealPlanService {
|
||||||
return this.mealPlanRepository.findByWeek(householdId, weekStartDate);
|
return this.mealPlanRepository.findByWeek(householdId, weekStartDate);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async create(
|
public async create(householdId: string, createdBy: string, input: CreateMealPlanInput) {
|
||||||
householdId: string,
|
|
||||||
createdBy: string,
|
|
||||||
input: CreateMealPlanInput
|
|
||||||
) {
|
|
||||||
// Prevent overlapping meal plans for same household/week
|
// Prevent overlapping meal plans for same household/week
|
||||||
const existing = await this.mealPlanRepository.findByWeek(
|
const existing = await this.mealPlanRepository.findByWeek(householdId, input.weekStartDate);
|
||||||
householdId,
|
|
||||||
input.weekStartDate
|
|
||||||
);
|
|
||||||
if (existing) {
|
if (existing) {
|
||||||
throw new BadRequestError(
|
throw new BadRequestError(
|
||||||
`A meal plan already exists for household ${householdId} starting ${input.weekStartDate}`
|
`A meal plan already exists for household ${householdId} starting ${input.weekStartDate}`,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -66,11 +59,7 @@ export class MealPlanService {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public async update(
|
public async update(id: string, householdId: string, input: UpdateMealPlanInput) {
|
||||||
id: string,
|
|
||||||
householdId: string,
|
|
||||||
input: UpdateMealPlanInput
|
|
||||||
) {
|
|
||||||
const existing = await this.getById(id, householdId);
|
const existing = await this.getById(id, householdId);
|
||||||
|
|
||||||
const data: Record<string, unknown> = {};
|
const data: Record<string, unknown> = {};
|
||||||
|
|
@ -90,11 +79,7 @@ export class MealPlanService {
|
||||||
return updated;
|
return updated;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async updateStatus(
|
public async updateStatus(id: string, householdId: string, status: MealPlanStatus) {
|
||||||
id: string,
|
|
||||||
householdId: string,
|
|
||||||
status: MealPlanStatus
|
|
||||||
) {
|
|
||||||
await this.getById(id, householdId);
|
await this.getById(id, householdId);
|
||||||
const updated = await this.mealPlanRepository.updateStatus(id, householdId, status);
|
const updated = await this.mealPlanRepository.updateStatus(id, householdId, status);
|
||||||
if (!updated) {
|
if (!updated) {
|
||||||
|
|
|
||||||
|
|
@ -44,10 +44,7 @@ export class ShoppingGapService {
|
||||||
this.productsRepository = productsRepository;
|
this.productsRepository = productsRepository;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async calculateGap(
|
public async calculateGap(householdId: string, mealPlanId: string): Promise<ShoppingGapResult> {
|
||||||
householdId: string,
|
|
||||||
mealPlanId: string
|
|
||||||
): Promise<ShoppingGapResult> {
|
|
||||||
const plan = await this.mealPlanRepository.findById(mealPlanId, householdId);
|
const plan = await this.mealPlanRepository.findById(mealPlanId, householdId);
|
||||||
if (!plan) {
|
if (!plan) {
|
||||||
throw new NotFoundError('Meal plan not found');
|
throw new NotFoundError('Meal plan not found');
|
||||||
|
|
@ -75,7 +72,7 @@ export class ShoppingGapService {
|
||||||
// 2. Fetch all referenced recipes in parallel
|
// 2. Fetch all referenced recipes in parallel
|
||||||
const recipeIds = Array.from(recipeIdSet);
|
const recipeIds = Array.from(recipeIdSet);
|
||||||
const recipeDocs = await Promise.all(
|
const recipeDocs = await Promise.all(
|
||||||
recipeIds.map((id) => this.recipesRepository.findById(id, householdId))
|
recipeIds.map((id) => this.recipesRepository.findById(id, householdId)),
|
||||||
);
|
);
|
||||||
const recipesMap = new Map<string, any>();
|
const recipesMap = new Map<string, any>();
|
||||||
for (const doc of recipeDocs) {
|
for (const doc of recipeDocs) {
|
||||||
|
|
|
||||||
|
|
@ -44,7 +44,7 @@ export class SuggestionEngineService {
|
||||||
public async getSuggestions(
|
public async getSuggestions(
|
||||||
householdId: string,
|
householdId: string,
|
||||||
userId: string,
|
userId: string,
|
||||||
options: { limit?: number } = {}
|
options: { limit?: number } = {},
|
||||||
): Promise<ScoredRecipe[]> {
|
): Promise<ScoredRecipe[]> {
|
||||||
const limit = options.limit ?? 5;
|
const limit = options.limit ?? 5;
|
||||||
|
|
||||||
|
|
@ -132,7 +132,7 @@ export class SuggestionEngineService {
|
||||||
recipeId,
|
recipeId,
|
||||||
pantryInventory,
|
pantryInventory,
|
||||||
activeTarget as Record<string, unknown> | null,
|
activeTarget as Record<string, unknown> | null,
|
||||||
lastEaten
|
lastEaten,
|
||||||
);
|
);
|
||||||
|
|
||||||
const totalScore =
|
const totalScore =
|
||||||
|
|
@ -153,9 +153,7 @@ export class SuggestionEngineService {
|
||||||
}
|
}
|
||||||
|
|
||||||
// 5. Sort descending and limit
|
// 5. Sort descending and limit
|
||||||
return scoredList
|
return scoredList.sort((a, b) => b.totalScore - a.totalScore).slice(0, limit);
|
||||||
.sort((a, b) => b.totalScore - a.totalScore)
|
|
||||||
.slice(0, limit);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private scoreRecipe(
|
private scoreRecipe(
|
||||||
|
|
@ -164,7 +162,7 @@ export class SuggestionEngineService {
|
||||||
recipeId: string,
|
recipeId: string,
|
||||||
inventory: Map<string, { qty: number; minDays: number; maxUrgencyWeight: number }>,
|
inventory: Map<string, { qty: number; minDays: number; maxUrgencyWeight: number }>,
|
||||||
target: Record<string, unknown> | null,
|
target: Record<string, unknown> | null,
|
||||||
lastEaten: Map<string, number>
|
lastEaten: Map<string, number>,
|
||||||
) {
|
) {
|
||||||
// Filter non-optional ingredients
|
// Filter non-optional ingredients
|
||||||
const requiredIngs = ingredients.filter((ing) => !ing.isOptional);
|
const requiredIngs = ingredients.filter((ing) => !ing.isOptional);
|
||||||
|
|
@ -262,7 +260,7 @@ export class SuggestionEngineService {
|
||||||
|
|
||||||
private generateReasoning(
|
private generateReasoning(
|
||||||
scores: { coverage: number; urgency: number; nutrition: number; variety: number },
|
scores: { coverage: number; urgency: number; nutrition: number; variety: number },
|
||||||
recipeName: string
|
recipeName: string,
|
||||||
): string[] {
|
): string[] {
|
||||||
const reasons: string[] = [];
|
const reasons: string[] = [];
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -2,16 +2,11 @@ import { NutritionTargetModel } from '../../schemas/nutrition-target.schema.js';
|
||||||
|
|
||||||
export class NutritionTargetRepository {
|
export class NutritionTargetRepository {
|
||||||
public async findByUser(userId: string, householdId: string) {
|
public async findByUser(userId: string, householdId: string) {
|
||||||
return NutritionTargetModel.findOne({ userId, householdId, isActive: true })
|
return NutritionTargetModel.findOne({ userId, householdId, isActive: true }).lean().exec();
|
||||||
.lean()
|
|
||||||
.exec();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public async findAllByUser(userId: string, householdId: string) {
|
public async findAllByUser(userId: string, householdId: string) {
|
||||||
return NutritionTargetModel.find({ userId, householdId })
|
return NutritionTargetModel.find({ userId, householdId }).sort({ createdAt: -1 }).lean().exec();
|
||||||
.sort({ createdAt: -1 })
|
|
||||||
.lean()
|
|
||||||
.exec();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public async create(data: Record<string, unknown>) {
|
public async create(data: Record<string, unknown>) {
|
||||||
|
|
@ -23,15 +18,20 @@ export class NutritionTargetRepository {
|
||||||
public async deactivateAllForUser(userId: string, householdId: string) {
|
public async deactivateAllForUser(userId: string, householdId: string) {
|
||||||
return NutritionTargetModel.updateMany(
|
return NutritionTargetModel.updateMany(
|
||||||
{ userId, householdId, isActive: true },
|
{ userId, householdId, isActive: true },
|
||||||
{ $set: { isActive: false } }
|
{ $set: { isActive: false } },
|
||||||
).exec();
|
).exec();
|
||||||
}
|
}
|
||||||
|
|
||||||
public async update(id: string, userId: string, householdId: string, data: Record<string, unknown>) {
|
public async update(
|
||||||
|
id: string,
|
||||||
|
userId: string,
|
||||||
|
householdId: string,
|
||||||
|
data: Record<string, unknown>,
|
||||||
|
) {
|
||||||
return NutritionTargetModel.findOneAndUpdate(
|
return NutritionTargetModel.findOneAndUpdate(
|
||||||
{ _id: id, userId, householdId },
|
{ _id: id, userId, householdId },
|
||||||
{ $set: data },
|
{ $set: data },
|
||||||
{ new: true, lean: true }
|
{ new: true, lean: true },
|
||||||
).exec();
|
).exec();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,10 +2,7 @@ import fp from 'fastify-plugin';
|
||||||
import { asClass, Lifetime } from 'awilix';
|
import { asClass, Lifetime } from 'awilix';
|
||||||
import type { ZodTypeProvider } from 'fastify-type-provider-zod';
|
import type { ZodTypeProvider } from 'fastify-type-provider-zod';
|
||||||
import { z } from 'zod/v4';
|
import { z } from 'zod/v4';
|
||||||
import {
|
import { NutritionTargetSchema, NutritionTargetResponseSchema } from '@meshitrack/shared';
|
||||||
NutritionTargetSchema,
|
|
||||||
NutritionTargetResponseSchema,
|
|
||||||
} from '@meshitrack/shared';
|
|
||||||
import { NutritionTargetRepository } from './nutrition-target.repository.js';
|
import { NutritionTargetRepository } from './nutrition-target.repository.js';
|
||||||
import { NutritionTargetService } from './nutrition-target.service.js';
|
import { NutritionTargetService } from './nutrition-target.service.js';
|
||||||
|
|
||||||
|
|
@ -33,7 +30,9 @@ function toIso(v: string | Date): string {
|
||||||
return typeof v === 'string' ? v : v.toISOString();
|
return typeof v === 'string' ? v : v.toISOString();
|
||||||
}
|
}
|
||||||
|
|
||||||
function toNutritionTargetResponse(doc: AnyTargetDoc): z.infer<typeof NutritionTargetResponseSchema> {
|
function toNutritionTargetResponse(
|
||||||
|
doc: AnyTargetDoc,
|
||||||
|
): z.infer<typeof NutritionTargetResponseSchema> {
|
||||||
return {
|
return {
|
||||||
_id: toStr(doc._id),
|
_id: toStr(doc._id),
|
||||||
userId: doc.userId,
|
userId: doc.userId,
|
||||||
|
|
@ -61,7 +60,9 @@ declare module '@fastify/awilix' {
|
||||||
export default fp(
|
export default fp(
|
||||||
async (fastify) => {
|
async (fastify) => {
|
||||||
fastify.diContainer.register({
|
fastify.diContainer.register({
|
||||||
nutritionTargetRepository: asClass(NutritionTargetRepository, { lifetime: Lifetime.SINGLETON }),
|
nutritionTargetRepository: asClass(NutritionTargetRepository, {
|
||||||
|
lifetime: Lifetime.SINGLETON,
|
||||||
|
}),
|
||||||
nutritionTargetService: asClass(NutritionTargetService, { lifetime: Lifetime.SINGLETON }),
|
nutritionTargetService: asClass(NutritionTargetService, { lifetime: Lifetime.SINGLETON }),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -124,11 +125,7 @@ export default fp(
|
||||||
handler: async (request, reply) => {
|
handler: async (request, reply) => {
|
||||||
const service = fastify.diContainer.resolve('nutritionTargetService');
|
const service = fastify.diContainer.resolve('nutritionTargetService');
|
||||||
const userId = request.user.keycloakId;
|
const userId = request.user.keycloakId;
|
||||||
const target = await service.setTarget(
|
const target = await service.setTarget(userId, request.params.householdId, request.body);
|
||||||
userId,
|
|
||||||
request.params.householdId,
|
|
||||||
request.body
|
|
||||||
);
|
|
||||||
return reply.status(201).send(toNutritionTargetResponse(target as AnyTargetDoc));
|
return reply.status(201).send(toNutritionTargetResponse(target as AnyTargetDoc));
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
@ -155,5 +152,5 @@ export default fp(
|
||||||
{
|
{
|
||||||
name: 'nutrition-targets-routes',
|
name: 'nutrition-targets-routes',
|
||||||
dependencies: ['auth-plugin'],
|
dependencies: ['auth-plugin'],
|
||||||
}
|
},
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -22,11 +22,7 @@ export class NutritionTargetService {
|
||||||
return this.nutritionTargetRepository.findAllByUser(userId, householdId);
|
return this.nutritionTargetRepository.findAllByUser(userId, householdId);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async setTarget(
|
public async setTarget(userId: string, householdId: string, input: SetNutritionTargetInput) {
|
||||||
userId: string,
|
|
||||||
householdId: string,
|
|
||||||
input: SetNutritionTargetInput
|
|
||||||
) {
|
|
||||||
// Maintain invariant: only one target is active per user per household
|
// Maintain invariant: only one target is active per user per household
|
||||||
if (input.isActive !== false) {
|
if (input.isActive !== false) {
|
||||||
await this.nutritionTargetRepository.deactivateAllForUser(userId, householdId);
|
await this.nutritionTargetRepository.deactivateAllForUser(userId, householdId);
|
||||||
|
|
@ -52,20 +48,20 @@ export class NutritionTargetService {
|
||||||
|
|
||||||
switch (preset) {
|
switch (preset) {
|
||||||
case 'loss':
|
case 'loss':
|
||||||
proteinPct = 0.40;
|
proteinPct = 0.4;
|
||||||
carbsPct = 0.30;
|
carbsPct = 0.3;
|
||||||
fatPct = 0.30;
|
fatPct = 0.3;
|
||||||
break;
|
break;
|
||||||
case 'gain':
|
case 'gain':
|
||||||
proteinPct = 0.25;
|
proteinPct = 0.25;
|
||||||
carbsPct = 0.50;
|
carbsPct = 0.5;
|
||||||
fatPct = 0.25;
|
fatPct = 0.25;
|
||||||
break;
|
break;
|
||||||
case 'maintenance':
|
case 'maintenance':
|
||||||
default:
|
default:
|
||||||
proteinPct = 0.30;
|
proteinPct = 0.3;
|
||||||
carbsPct = 0.40;
|
carbsPct = 0.4;
|
||||||
fatPct = 0.30;
|
fatPct = 0.3;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -27,13 +27,13 @@ export class PricesRepository {
|
||||||
|
|
||||||
public async createMany(data: CreatePriceRecordData[]) {
|
public async createMany(data: CreatePriceRecordData[]) {
|
||||||
const records = await PriceRecordModel.insertMany(data);
|
const records = await PriceRecordModel.insertMany(data);
|
||||||
return records.map(r => r.toObject());
|
return records.map((r) => r.toObject());
|
||||||
}
|
}
|
||||||
|
|
||||||
public async findByProduct(
|
public async findByProduct(
|
||||||
householdId: string,
|
householdId: string,
|
||||||
productId: string,
|
productId: string,
|
||||||
query: PriceHistoryQueryInput
|
query: PriceHistoryQueryInput,
|
||||||
) {
|
) {
|
||||||
const filter: Record<string, unknown> = { householdId, productId };
|
const filter: Record<string, unknown> = { householdId, productId };
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -95,7 +95,7 @@ export default fp(
|
||||||
const record = await service.recordPrice(
|
const record = await service.recordPrice(
|
||||||
request.body,
|
request.body,
|
||||||
request.params.householdId,
|
request.params.householdId,
|
||||||
request.user.keycloakId
|
request.user.keycloakId,
|
||||||
);
|
);
|
||||||
return reply.status(201).send(toPriceRecordResponse(record));
|
return reply.status(201).send(toPriceRecordResponse(record));
|
||||||
},
|
},
|
||||||
|
|
@ -114,7 +114,7 @@ export default fp(
|
||||||
const records = await service.recordBulkPrices(
|
const records = await service.recordBulkPrices(
|
||||||
request.body,
|
request.body,
|
||||||
request.params.householdId,
|
request.params.householdId,
|
||||||
request.user.keycloakId
|
request.user.keycloakId,
|
||||||
);
|
);
|
||||||
return reply.status(201).send(records.map(toPriceRecordResponse));
|
return reply.status(201).send(records.map(toPriceRecordResponse));
|
||||||
},
|
},
|
||||||
|
|
@ -133,7 +133,7 @@ export default fp(
|
||||||
const result = await service.getPriceHistory(
|
const result = await service.getPriceHistory(
|
||||||
request.params.productId,
|
request.params.productId,
|
||||||
request.params.householdId,
|
request.params.householdId,
|
||||||
request.query
|
request.query,
|
||||||
);
|
);
|
||||||
return reply.send({
|
return reply.send({
|
||||||
data: result.data.map(toPriceRecordResponse),
|
data: result.data.map(toPriceRecordResponse),
|
||||||
|
|
@ -153,7 +153,7 @@ export default fp(
|
||||||
const service = fastify.diContainer.resolve('pricesService');
|
const service = fastify.diContainer.resolve('pricesService');
|
||||||
const results = await service.compareStores(
|
const results = await service.compareStores(
|
||||||
request.params.productId,
|
request.params.productId,
|
||||||
request.params.householdId
|
request.params.householdId,
|
||||||
);
|
);
|
||||||
return reply.send({
|
return reply.send({
|
||||||
data: results.map((r) => ({
|
data: results.map((r) => ({
|
||||||
|
|
@ -184,5 +184,5 @@ export default fp(
|
||||||
{
|
{
|
||||||
name: 'prices-routes',
|
name: 'prices-routes',
|
||||||
dependencies: ['auth-plugin'],
|
dependencies: ['auth-plugin'],
|
||||||
}
|
},
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -28,11 +28,7 @@ export class PricesService {
|
||||||
/**
|
/**
|
||||||
* Validates entity existence, computes pricePerUnit, and persists record
|
* Validates entity existence, computes pricePerUnit, and persists record
|
||||||
*/
|
*/
|
||||||
public async recordPrice(
|
public async recordPrice(data: CreatePriceRecordInput, householdId: string, userId: string) {
|
||||||
data: CreatePriceRecordInput,
|
|
||||||
householdId: string,
|
|
||||||
userId: string
|
|
||||||
) {
|
|
||||||
const [product, store] = await Promise.all([
|
const [product, store] = await Promise.all([
|
||||||
this.productsRepository.findById(data.productId, householdId),
|
this.productsRepository.findById(data.productId, householdId),
|
||||||
this.storesRepository.findById(data.storeId, householdId),
|
this.storesRepository.findById(data.storeId, householdId),
|
||||||
|
|
@ -64,11 +60,7 @@ export class PricesService {
|
||||||
/**
|
/**
|
||||||
* Ingests a list of purchased products in a single transaction
|
* Ingests a list of purchased products in a single transaction
|
||||||
*/
|
*/
|
||||||
public async recordBulkPrices(
|
public async recordBulkPrices(data: BulkPriceRecordInput, householdId: string, userId: string) {
|
||||||
data: BulkPriceRecordInput,
|
|
||||||
householdId: string,
|
|
||||||
userId: string
|
|
||||||
) {
|
|
||||||
const store = await this.storesRepository.findById(data.storeId, householdId);
|
const store = await this.storesRepository.findById(data.storeId, householdId);
|
||||||
if (!store) throw new NotFoundError(`Store not found: ${data.storeId}`);
|
if (!store) throw new NotFoundError(`Store not found: ${data.storeId}`);
|
||||||
|
|
||||||
|
|
@ -109,7 +101,7 @@ export class PricesService {
|
||||||
public async getPriceHistory(
|
public async getPriceHistory(
|
||||||
productId: string,
|
productId: string,
|
||||||
householdId: string,
|
householdId: string,
|
||||||
query: PriceHistoryQueryInput
|
query: PriceHistoryQueryInput,
|
||||||
) {
|
) {
|
||||||
return this.pricesRepository.findByProduct(householdId, productId, query);
|
return this.pricesRepository.findByProduct(householdId, productId, query);
|
||||||
}
|
}
|
||||||
|
|
@ -129,13 +121,16 @@ export class PricesService {
|
||||||
public async estimatePrice(
|
public async estimatePrice(
|
||||||
productId: string,
|
productId: string,
|
||||||
householdId: string,
|
householdId: string,
|
||||||
storeId?: string
|
storeId?: string,
|
||||||
): Promise<number | null> {
|
): Promise<number | null> {
|
||||||
const latest = await this.pricesRepository.getLatestForProduct(householdId, productId, storeId);
|
const latest = await this.pricesRepository.getLatestForProduct(householdId, productId, storeId);
|
||||||
if (!latest) {
|
if (!latest) {
|
||||||
// If a specific store was requested but has no history, fall back to the generic latest across all stores
|
// If a specific store was requested but has no history, fall back to the generic latest across all stores
|
||||||
if (storeId) {
|
if (storeId) {
|
||||||
const genericLatest = await this.pricesRepository.getLatestForProduct(householdId, productId);
|
const genericLatest = await this.pricesRepository.getLatestForProduct(
|
||||||
|
householdId,
|
||||||
|
productId,
|
||||||
|
);
|
||||||
return genericLatest ? genericLatest.price : null;
|
return genericLatest ? genericLatest.price : null;
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
|
|
|
||||||
|
|
@ -34,7 +34,7 @@ export class ShoppingListsRepository {
|
||||||
.sort({ createdAt: -1 })
|
.sort({ createdAt: -1 })
|
||||||
.lean()
|
.lean()
|
||||||
.exec();
|
.exec();
|
||||||
return lists.map(l => this.sortItems(l));
|
return lists.map((l) => this.sortItems(l));
|
||||||
}
|
}
|
||||||
|
|
||||||
public async findById(id: string, householdId: string) {
|
public async findById(id: string, householdId: string) {
|
||||||
|
|
@ -43,19 +43,24 @@ export class ShoppingListsRepository {
|
||||||
}
|
}
|
||||||
|
|
||||||
public async findActiveByHousehold(householdId: string) {
|
public async findActiveByHousehold(householdId: string) {
|
||||||
const lists = await ShoppingListModel.find({ householdId, status: { $in: ['active', 'shopping'] } })
|
const lists = await ShoppingListModel.find({
|
||||||
|
householdId,
|
||||||
|
status: { $in: ['active', 'shopping'] },
|
||||||
|
})
|
||||||
.sort({ updatedAt: -1 })
|
.sort({ updatedAt: -1 })
|
||||||
.lean()
|
.lean()
|
||||||
.exec();
|
.exec();
|
||||||
return lists.map(l => this.sortItems(l));
|
return lists.map((l) => this.sortItems(l));
|
||||||
}
|
}
|
||||||
|
|
||||||
public async update(id: string, householdId: string, data: UpdateShoppingListInput) {
|
public async update(id: string, householdId: string, data: UpdateShoppingListInput) {
|
||||||
const updated = await ShoppingListModel.findOneAndUpdate(
|
const updated = await ShoppingListModel.findOneAndUpdate(
|
||||||
{ _id: id, householdId },
|
{ _id: id, householdId },
|
||||||
{ $set: data },
|
{ $set: data },
|
||||||
{ new: true }
|
{ new: true },
|
||||||
).lean().exec();
|
)
|
||||||
|
.lean()
|
||||||
|
.exec();
|
||||||
return this.sortItems(updated);
|
return this.sortItems(updated);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -69,8 +74,10 @@ export class ShoppingListsRepository {
|
||||||
const updated = await ShoppingListModel.findOneAndUpdate(
|
const updated = await ShoppingListModel.findOneAndUpdate(
|
||||||
{ _id: id, householdId },
|
{ _id: id, householdId },
|
||||||
{ $push: { items: item } },
|
{ $push: { items: item } },
|
||||||
{ new: true }
|
{ new: true },
|
||||||
).lean().exec();
|
)
|
||||||
|
.lean()
|
||||||
|
.exec();
|
||||||
return this.sortItems(updated);
|
return this.sortItems(updated);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -78,7 +85,7 @@ export class ShoppingListsRepository {
|
||||||
id: string,
|
id: string,
|
||||||
householdId: string,
|
householdId: string,
|
||||||
itemId: string,
|
itemId: string,
|
||||||
updates: Partial<ShoppingItem>
|
updates: Partial<ShoppingItem>,
|
||||||
) {
|
) {
|
||||||
const setUpdates: Record<string, unknown> = {};
|
const setUpdates: Record<string, unknown> = {};
|
||||||
for (const [key, val] of Object.entries(updates)) {
|
for (const [key, val] of Object.entries(updates)) {
|
||||||
|
|
@ -88,8 +95,10 @@ export class ShoppingListsRepository {
|
||||||
const updated = await ShoppingListModel.findOneAndUpdate(
|
const updated = await ShoppingListModel.findOneAndUpdate(
|
||||||
{ _id: id, householdId, 'items.id': itemId },
|
{ _id: id, householdId, 'items.id': itemId },
|
||||||
{ $set: setUpdates },
|
{ $set: setUpdates },
|
||||||
{ new: true }
|
{ new: true },
|
||||||
).lean().exec();
|
)
|
||||||
|
.lean()
|
||||||
|
.exec();
|
||||||
return this.sortItems(updated);
|
return this.sortItems(updated);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -97,8 +106,10 @@ export class ShoppingListsRepository {
|
||||||
const updated = await ShoppingListModel.findOneAndUpdate(
|
const updated = await ShoppingListModel.findOneAndUpdate(
|
||||||
{ _id: id, householdId },
|
{ _id: id, householdId },
|
||||||
{ $pull: { items: { id: itemId } } },
|
{ $pull: { items: { id: itemId } } },
|
||||||
{ new: true }
|
{ new: true },
|
||||||
).lean().exec();
|
)
|
||||||
|
.lean()
|
||||||
|
.exec();
|
||||||
return this.sortItems(updated);
|
return this.sortItems(updated);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -114,7 +114,7 @@ export default fp(
|
||||||
const list = await service.create(
|
const list = await service.create(
|
||||||
request.body,
|
request.body,
|
||||||
request.params.householdId,
|
request.params.householdId,
|
||||||
request.user.keycloakId
|
request.user.keycloakId,
|
||||||
);
|
);
|
||||||
return reply.status(201).send(serializeList(list));
|
return reply.status(201).send(serializeList(list));
|
||||||
},
|
},
|
||||||
|
|
@ -147,7 +147,7 @@ export default fp(
|
||||||
const list = await service.update(
|
const list = await service.update(
|
||||||
request.params.id,
|
request.params.id,
|
||||||
request.params.householdId,
|
request.params.householdId,
|
||||||
request.body
|
request.body,
|
||||||
);
|
);
|
||||||
return reply.send(serializeList(list));
|
return reply.send(serializeList(list));
|
||||||
},
|
},
|
||||||
|
|
@ -181,7 +181,7 @@ export default fp(
|
||||||
const { list, addedItem } = await service.addItem(
|
const { list, addedItem } = await service.addItem(
|
||||||
request.params.id,
|
request.params.id,
|
||||||
request.params.householdId,
|
request.params.householdId,
|
||||||
request.body
|
request.body,
|
||||||
);
|
);
|
||||||
|
|
||||||
// Emit real-time update notification to existing connected viewers
|
// Emit real-time update notification to existing connected viewers
|
||||||
|
|
@ -209,11 +209,13 @@ export default fp(
|
||||||
request.params.householdId,
|
request.params.householdId,
|
||||||
request.params.itemId,
|
request.params.itemId,
|
||||||
request.body,
|
request.body,
|
||||||
request.user.keycloakId
|
request.user.keycloakId,
|
||||||
);
|
);
|
||||||
|
|
||||||
// Broadcast the precise item differential state update to sibling websocket listeners
|
// Broadcast the precise item differential state update to sibling websocket listeners
|
||||||
const matchedItem = updatedList.items.find((i: ShoppingItem) => i.id === request.params.itemId);
|
const matchedItem = updatedList.items.find(
|
||||||
|
(i: ShoppingItem) => i.id === request.params.itemId,
|
||||||
|
);
|
||||||
if (matchedItem) {
|
if (matchedItem) {
|
||||||
broadcastToList(request.params.id, null as any, {
|
broadcastToList(request.params.id, null as any, {
|
||||||
type: 'ITEM_UPDATED',
|
type: 'ITEM_UPDATED',
|
||||||
|
|
@ -242,7 +244,7 @@ export default fp(
|
||||||
const list = await service.removeItem(
|
const list = await service.removeItem(
|
||||||
request.params.id,
|
request.params.id,
|
||||||
request.params.householdId,
|
request.params.householdId,
|
||||||
request.params.itemId
|
request.params.itemId,
|
||||||
);
|
);
|
||||||
|
|
||||||
broadcastToList(request.params.id, null as any, {
|
broadcastToList(request.params.id, null as any, {
|
||||||
|
|
@ -268,7 +270,7 @@ export default fp(
|
||||||
const list = await service.createFromMealPlan(
|
const list = await service.createFromMealPlan(
|
||||||
request.params.mealPlanId,
|
request.params.mealPlanId,
|
||||||
request.params.householdId,
|
request.params.householdId,
|
||||||
request.user.keycloakId
|
request.user.keycloakId,
|
||||||
);
|
);
|
||||||
return reply.status(201).send(serializeList(list));
|
return reply.status(201).send(serializeList(list));
|
||||||
},
|
},
|
||||||
|
|
@ -286,7 +288,7 @@ export default fp(
|
||||||
const results = await service.syncCheckedToPantry(
|
const results = await service.syncCheckedToPantry(
|
||||||
request.params.id,
|
request.params.id,
|
||||||
request.params.householdId,
|
request.params.householdId,
|
||||||
request.user.keycloakId
|
request.user.keycloakId,
|
||||||
);
|
);
|
||||||
return reply.send(results);
|
return reply.send(results);
|
||||||
},
|
},
|
||||||
|
|
@ -303,7 +305,7 @@ export default fp(
|
||||||
const service = fastify.diContainer.resolve('shoppingListsService');
|
const service = fastify.diContainer.resolve('shoppingListsService');
|
||||||
const comparison = await service.getStoreComparison(
|
const comparison = await service.getStoreComparison(
|
||||||
request.params.id,
|
request.params.id,
|
||||||
request.params.householdId
|
request.params.householdId,
|
||||||
);
|
);
|
||||||
return reply.send(comparison);
|
return reply.send(comparison);
|
||||||
},
|
},
|
||||||
|
|
@ -339,10 +341,12 @@ export default fp(
|
||||||
request.params.householdId,
|
request.params.householdId,
|
||||||
payload.itemId,
|
payload.itemId,
|
||||||
{ checked: payload.checked },
|
{ checked: payload.checked },
|
||||||
request.user.keycloakId
|
request.user.keycloakId,
|
||||||
);
|
);
|
||||||
|
|
||||||
const matched = updatedList.items.find((it: ShoppingItem) => it.id === payload.itemId);
|
const matched = updatedList.items.find(
|
||||||
|
(it: ShoppingItem) => it.id === payload.itemId,
|
||||||
|
);
|
||||||
|
|
||||||
// Echo back differential confirmation to everyone else on the floor
|
// Echo back differential confirmation to everyone else on the floor
|
||||||
broadcastToList(listId, socket, {
|
broadcastToList(listId, socket, {
|
||||||
|
|
@ -352,7 +356,7 @@ export default fp(
|
||||||
checked: payload.checked,
|
checked: payload.checked,
|
||||||
checkedAt: matched?.checkedAt?.toISOString(),
|
checkedAt: matched?.checkedAt?.toISOString(),
|
||||||
checkedBy: matched?.checkedBy,
|
checkedBy: matched?.checkedBy,
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
|
@ -370,12 +374,12 @@ export default fp(
|
||||||
}
|
}
|
||||||
request.log.info({ listId }, 'Client severed sync handshake connection');
|
request.log.info({ listId }, 'Client severed sync handshake connection');
|
||||||
});
|
});
|
||||||
}
|
},
|
||||||
);
|
);
|
||||||
/* v8 ignore stop */
|
/* v8 ignore stop */
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'shopping-lists-routes',
|
name: 'shopping-lists-routes',
|
||||||
dependencies: ['auth-plugin'],
|
dependencies: ['auth-plugin'],
|
||||||
}
|
},
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -13,7 +13,7 @@ import type {
|
||||||
ShoppingItem,
|
ShoppingItem,
|
||||||
} from '@meshitrack/shared';
|
} from '@meshitrack/shared';
|
||||||
import { ShoppingListSourceType } from '@meshitrack/shared';
|
import { ShoppingListSourceType } from '@meshitrack/shared';
|
||||||
import { StorageLocation, ServingUnit } from '@meshitrack/shared';
|
import { StorageLocation, type ServingUnit } from '@meshitrack/shared';
|
||||||
import { NotFoundError } from '../../common/errors.js';
|
import { NotFoundError } from '../../common/errors.js';
|
||||||
import { v4 as uuidv4 } from 'uuid';
|
import { v4 as uuidv4 } from 'uuid';
|
||||||
|
|
||||||
|
|
@ -158,7 +158,7 @@ export class ShoppingListsService {
|
||||||
householdId: string,
|
householdId: string,
|
||||||
itemId: string,
|
itemId: string,
|
||||||
data: UpdateShoppingItemInput,
|
data: UpdateShoppingItemInput,
|
||||||
userId: string
|
userId: string,
|
||||||
) {
|
) {
|
||||||
const updates: Partial<ShoppingItem> = { ...data };
|
const updates: Partial<ShoppingItem> = { ...data };
|
||||||
|
|
||||||
|
|
@ -208,7 +208,10 @@ export class ShoppingListsService {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const dateStr = new Date((plan as any).weekStartDate).toLocaleDateString('en-US', { month: 'short', day: 'numeric' });
|
const dateStr = new Date((plan as any).weekStartDate).toLocaleDateString('en-US', {
|
||||||
|
month: 'short',
|
||||||
|
day: 'numeric',
|
||||||
|
});
|
||||||
const name = `Groceries for Week of ${dateStr}`;
|
const name = `Groceries for Week of ${dateStr}`;
|
||||||
|
|
||||||
const newList = await this.shoppingListsRepository.create({
|
const newList = await this.shoppingListsRepository.create({
|
||||||
|
|
@ -242,7 +245,9 @@ export class ShoppingListsService {
|
||||||
let addedCount = 0;
|
let addedCount = 0;
|
||||||
let pricesLogged = 0;
|
let pricesLogged = 0;
|
||||||
|
|
||||||
const pendingItems = list.items.filter((it: ShoppingItem) => it.checked && !it.addedToPantry && it.productId);
|
const pendingItems = list.items.filter(
|
||||||
|
(it: ShoppingItem) => it.checked && !it.addedToPantry && it.productId,
|
||||||
|
);
|
||||||
|
|
||||||
for (const item of pendingItems) {
|
for (const item of pendingItems) {
|
||||||
// 1. Promote item to active pantry
|
// 1. Promote item to active pantry
|
||||||
|
|
@ -257,7 +262,7 @@ export class ShoppingListsService {
|
||||||
notes: item.notes || undefined,
|
notes: item.notes || undefined,
|
||||||
},
|
},
|
||||||
householdId,
|
householdId,
|
||||||
userId
|
userId,
|
||||||
);
|
);
|
||||||
addedCount++;
|
addedCount++;
|
||||||
|
|
||||||
|
|
@ -275,7 +280,7 @@ export class ShoppingListsService {
|
||||||
currency: 'USD',
|
currency: 'USD',
|
||||||
},
|
},
|
||||||
householdId,
|
householdId,
|
||||||
userId
|
userId,
|
||||||
);
|
);
|
||||||
pricesLogged++;
|
pricesLogged++;
|
||||||
}
|
}
|
||||||
|
|
@ -347,7 +352,9 @@ export class ShoppingListsService {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Sort to surface the cheapest/fullest single store options first
|
// Sort to surface the cheapest/fullest single store options first
|
||||||
singleStoreOptions.sort((a, b) => b.itemsCovered - a.itemsCovered || a.estimatedTotal - b.estimatedTotal);
|
singleStoreOptions.sort(
|
||||||
|
(a, b) => b.itemsCovered - a.itemsCovered || a.estimatedTotal - b.estimatedTotal,
|
||||||
|
);
|
||||||
|
|
||||||
return { singleStoreOptions };
|
return { singleStoreOptions };
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -17,7 +17,7 @@ const priceRecordSchema = new mongoose.Schema(
|
||||||
notes: { type: String },
|
notes: { type: String },
|
||||||
createdBy: { type: String, required: true },
|
createdBy: { type: String, required: true },
|
||||||
},
|
},
|
||||||
{ timestamps: { createdAt: true, updatedAt: false } }
|
{ timestamps: { createdAt: true, updatedAt: false } },
|
||||||
);
|
);
|
||||||
|
|
||||||
// Performance Indexes for Lookup Speed and Aggregations
|
// Performance Indexes for Lookup Speed and Aggregations
|
||||||
|
|
|
||||||
|
|
@ -17,7 +17,7 @@ const shoppingItemSchema = new mongoose.Schema(
|
||||||
category: { type: String },
|
category: { type: String },
|
||||||
addedToPantry: { type: Boolean, required: true, default: false },
|
addedToPantry: { type: Boolean, required: true, default: false },
|
||||||
},
|
},
|
||||||
{ _id: false }
|
{ _id: false },
|
||||||
);
|
);
|
||||||
|
|
||||||
const shoppingListSourceSchema = new mongoose.Schema(
|
const shoppingListSourceSchema = new mongoose.Schema(
|
||||||
|
|
@ -25,7 +25,7 @@ const shoppingListSourceSchema = new mongoose.Schema(
|
||||||
type: { type: String, required: true }, // values from ShoppingListSourceType
|
type: { type: String, required: true }, // values from ShoppingListSourceType
|
||||||
referenceId: { type: String },
|
referenceId: { type: String },
|
||||||
},
|
},
|
||||||
{ _id: false }
|
{ _id: false },
|
||||||
);
|
);
|
||||||
|
|
||||||
const shoppingListSchema = new mongoose.Schema(
|
const shoppingListSchema = new mongoose.Schema(
|
||||||
|
|
@ -44,7 +44,7 @@ const shoppingListSchema = new mongoose.Schema(
|
||||||
completedAt: { type: Date },
|
completedAt: { type: Date },
|
||||||
createdBy: { type: String, required: true },
|
createdBy: { type: String, required: true },
|
||||||
},
|
},
|
||||||
{ timestamps: true }
|
{ timestamps: true },
|
||||||
);
|
);
|
||||||
|
|
||||||
shoppingListSchema.index({ householdId: 1, status: 1 });
|
shoppingListSchema.index({ householdId: 1, status: 1 });
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,7 @@ import {
|
||||||
ForbiddenError,
|
ForbiddenError,
|
||||||
ConflictError,
|
ConflictError,
|
||||||
BadRequestError,
|
BadRequestError,
|
||||||
} from './errors.js';
|
} from '../../src/common/errors.js';
|
||||||
|
|
||||||
describe(AppError.name, () => {
|
describe(AppError.name, () => {
|
||||||
it('sets statusCode, error, message, and details', () => {
|
it('sets statusCode, error, message, and details', () => {
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import { describe, it, expect } from 'vitest';
|
import { describe, it, expect } from 'vitest';
|
||||||
import config from './configuration.js';
|
import config from '../../src/config/configuration.js';
|
||||||
|
|
||||||
describe('configuration', () => {
|
describe('configuration', () => {
|
||||||
it('exports default config values', () => {
|
it('exports default config values', () => {
|
||||||
34
packages/api/tests/helpers/mock-repository.ts
Normal file
34
packages/api/tests/helpers/mock-repository.ts
Normal file
|
|
@ -0,0 +1,34 @@
|
||||||
|
import { vi } from 'vitest';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Dynamically creates a fully mocked repository from a repository class.
|
||||||
|
* Recursively walks the prototype chain (inheritance-aware) to gather all methods,
|
||||||
|
* and assigns them a Vitest mock function (vi.fn()).
|
||||||
|
*
|
||||||
|
* @param repoClass The repository class constructor to mock
|
||||||
|
* @returns An object with all methods mocked as vi.fn()
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* const mockRepo = createMockRepository(ProductsRepository);
|
||||||
|
* mockRepo.findById.mockResolvedValue(mockProduct);
|
||||||
|
*/
|
||||||
|
export function createMockRepository<T>(
|
||||||
|
repoClass: new (...args: any[]) => T
|
||||||
|
): Record<keyof T, any> {
|
||||||
|
const mock: Record<string, any> = {};
|
||||||
|
let proto = repoClass.prototype;
|
||||||
|
|
||||||
|
while (proto && proto !== Object.prototype) {
|
||||||
|
const methods = Object.getOwnPropertyNames(proto).filter(
|
||||||
|
(name) => name !== 'constructor' && typeof (proto as any)[name] === 'function'
|
||||||
|
);
|
||||||
|
for (const method of methods) {
|
||||||
|
if (!(method in mock)) {
|
||||||
|
mock[method] = vi.fn();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
proto = Object.getPrototypeOf(proto);
|
||||||
|
}
|
||||||
|
|
||||||
|
return mock as Record<keyof T, any>;
|
||||||
|
}
|
||||||
|
|
@ -63,9 +63,9 @@ vi.mock('jose', () => ({
|
||||||
jwtVerify: vi.fn(),
|
jwtVerify: vi.fn(),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
import { buildApp } from './main.js';
|
import { buildApp } from '../src/main.js';
|
||||||
import * as jose from 'jose';
|
import * as jose from 'jose';
|
||||||
import { NotFoundError } from './common/errors.js';
|
import { NotFoundError } from '../src/common/errors.js';
|
||||||
|
|
||||||
describe('buildApp', () => {
|
describe('buildApp', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
|
|
@ -7,7 +7,7 @@ const { mockFind, mockSave, mockInsertMany, mockAggregate } = vi.hoisted(() => (
|
||||||
mockAggregate: vi.fn(),
|
mockAggregate: vi.fn(),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
vi.mock('../../schemas/cabinet-event.schema.js', () => {
|
vi.mock('../../../src/schemas/cabinet-event.schema.js', () => {
|
||||||
const chain = () => ({
|
const chain = () => ({
|
||||||
sort: vi.fn().mockReturnThis(),
|
sort: vi.fn().mockReturnThis(),
|
||||||
limit: vi.fn().mockReturnThis(),
|
limit: vi.fn().mockReturnThis(),
|
||||||
|
|
@ -36,7 +36,7 @@ vi.mock('../../schemas/cabinet-event.schema.js', () => {
|
||||||
return { CabinetEventModel: FakeModel };
|
return { CabinetEventModel: FakeModel };
|
||||||
});
|
});
|
||||||
|
|
||||||
import { CabinetEventsRepository } from './cabinet-events.repository.js';
|
import { CabinetEventsRepository } from '../../../src/modules/cabinet-events/cabinet-events.repository.js';
|
||||||
|
|
||||||
describe(CabinetEventsRepository.name, () => {
|
describe(CabinetEventsRepository.name, () => {
|
||||||
let repo: CabinetEventsRepository;
|
let repo: CabinetEventsRepository;
|
||||||
|
|
@ -24,7 +24,7 @@ const { mockListEvents, mockGetEventsByItem, mockGetSpendingSummary } = vi.hoist
|
||||||
mockGetSpendingSummary: vi.fn(),
|
mockGetSpendingSummary: vi.fn(),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
vi.mock('./cabinet-events.repository.js', () => ({
|
vi.mock('../../../src/modules/cabinet-events/cabinet-events.repository.js', () => ({
|
||||||
CabinetEventsRepository: class {
|
CabinetEventsRepository: class {
|
||||||
create = vi.fn();
|
create = vi.fn();
|
||||||
createMany = vi.fn();
|
createMany = vi.fn();
|
||||||
|
|
@ -35,7 +35,7 @@ vi.mock('./cabinet-events.repository.js', () => ({
|
||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
|
|
||||||
vi.mock('./cabinet-events.service.js', () => ({
|
vi.mock('../../../src/modules/cabinet-events/cabinet-events.service.js', () => ({
|
||||||
CabinetEventsService: class {
|
CabinetEventsService: class {
|
||||||
logEvent = vi.fn();
|
logEvent = vi.fn();
|
||||||
logEvents = vi.fn();
|
logEvents = vi.fn();
|
||||||
|
|
@ -46,17 +46,17 @@ vi.mock('./cabinet-events.service.js', () => ({
|
||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
|
|
||||||
vi.mock('../users/users.repository.js', () => ({
|
vi.mock('../../../src/modules/users/users.repository.js', () => ({
|
||||||
UsersRepository: class {
|
UsersRepository: class {
|
||||||
findByKeycloakId = vi.fn().mockResolvedValue({ householdIds: ['hh1'] });
|
findByKeycloakId = vi.fn().mockResolvedValue({ householdIds: ['hh1'] });
|
||||||
upsertFromToken = vi.fn().mockResolvedValue({ householdIds: ['hh1'] });
|
upsertFromToken = vi.fn().mockResolvedValue({ householdIds: ['hh1'] });
|
||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
|
|
||||||
import authPlugin from '../../plugins/auth.plugin.js';
|
import authPlugin from '../../../src/plugins/auth.plugin.js';
|
||||||
import householdPlugin from '../../plugins/household.plugin.js';
|
import householdPlugin from '../../../src/plugins/household.plugin.js';
|
||||||
import usersRoutes from '../users/users.routes.js';
|
import usersRoutes from '../../../src/modules/users/users.routes.js';
|
||||||
import cabinetEventsRoutes from './cabinet-events.routes.js';
|
import cabinetEventsRoutes from '../../../src/modules/cabinet-events/cabinet-events.routes.js';
|
||||||
|
|
||||||
function makeFakeEvent(overrides = {}) {
|
function makeFakeEvent(overrides = {}) {
|
||||||
return {
|
return {
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||||
import { CabinetEventsService } from './cabinet-events.service.js';
|
import { CabinetEventsService } from '../../../src/modules/cabinet-events/cabinet-events.service.js';
|
||||||
|
|
||||||
describe(CabinetEventsService.name, () => {
|
describe(CabinetEventsService.name, () => {
|
||||||
const mockCabinetEventsRepo = {
|
const mockCabinetEventsRepo = {
|
||||||
|
|
@ -10,7 +10,7 @@ const { mockFind, mockFindOne, mockFindOneAndUpdate, mockSave, mockCountDocument
|
||||||
mockAggregate: vi.fn(),
|
mockAggregate: vi.fn(),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
vi.mock('../../schemas/cabinet-item.schema.js', () => {
|
vi.mock('../../../src/schemas/cabinet-item.schema.js', () => {
|
||||||
const chain = () => ({
|
const chain = () => ({
|
||||||
sort: vi.fn().mockReturnThis(),
|
sort: vi.fn().mockReturnThis(),
|
||||||
limit: vi.fn().mockReturnThis(),
|
limit: vi.fn().mockReturnThis(),
|
||||||
|
|
@ -54,7 +54,7 @@ vi.mock('../../schemas/cabinet-item.schema.js', () => {
|
||||||
return { CabinetItemModel: FakeModel };
|
return { CabinetItemModel: FakeModel };
|
||||||
});
|
});
|
||||||
|
|
||||||
import { CabinetRepository } from './cabinet.repository.js';
|
import { CabinetRepository } from '../../../src/modules/cabinet/cabinet.repository.js';
|
||||||
|
|
||||||
describe(CabinetRepository.name, () => {
|
describe(CabinetRepository.name, () => {
|
||||||
let repo: CabinetRepository;
|
let repo: CabinetRepository;
|
||||||
|
|
@ -45,7 +45,7 @@ const {
|
||||||
mockFindActiveByMedicineForFEFO: vi.fn(),
|
mockFindActiveByMedicineForFEFO: vi.fn(),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
vi.mock('./cabinet.repository.js', () => ({
|
vi.mock('../../../src/modules/cabinet/cabinet.repository.js', () => ({
|
||||||
CabinetRepository: class {
|
CabinetRepository: class {
|
||||||
findByHousehold = mockFindByHousehold;
|
findByHousehold = mockFindByHousehold;
|
||||||
findById = mockFindById;
|
findById = mockFindById;
|
||||||
|
|
@ -65,7 +65,7 @@ const { mockMedicineFindById } = vi.hoisted(() => ({
|
||||||
mockMedicineFindById: vi.fn(),
|
mockMedicineFindById: vi.fn(),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
vi.mock('../medicines/medicines.repository.js', () => ({
|
vi.mock('../../../src/modules/medicines/medicines.repository.js', () => ({
|
||||||
MedicinesRepository: class {
|
MedicinesRepository: class {
|
||||||
findById = mockMedicineFindById;
|
findById = mockMedicineFindById;
|
||||||
findByHousehold = vi.fn();
|
findByHousehold = vi.fn();
|
||||||
|
|
@ -80,7 +80,7 @@ const { mockProductFindById } = vi.hoisted(() => ({
|
||||||
mockProductFindById: vi.fn(),
|
mockProductFindById: vi.fn(),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
vi.mock('../medicine-products/medicine-products.repository.js', () => ({
|
vi.mock('../../../src/modules/medicine-products/medicine-products.repository.js', () => ({
|
||||||
MedicineProductsRepository: class {
|
MedicineProductsRepository: class {
|
||||||
findById = mockProductFindById;
|
findById = mockProductFindById;
|
||||||
findByMedicine = vi.fn();
|
findByMedicine = vi.fn();
|
||||||
|
|
@ -91,7 +91,7 @@ vi.mock('../medicine-products/medicine-products.repository.js', () => ({
|
||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
|
|
||||||
vi.mock('../medicine-products/medicine-products.service.js', () => ({
|
vi.mock('../../../src/modules/medicine-products/medicine-products.service.js', () => ({
|
||||||
MedicineProductsService: class {
|
MedicineProductsService: class {
|
||||||
listByMedicine = vi.fn();
|
listByMedicine = vi.fn();
|
||||||
getById = vi.fn();
|
getById = vi.fn();
|
||||||
|
|
@ -101,7 +101,7 @@ vi.mock('../medicine-products/medicine-products.service.js', () => ({
|
||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
|
|
||||||
vi.mock('../medicines/medicines.service.js', () => ({
|
vi.mock('../../../src/modules/medicines/medicines.service.js', () => ({
|
||||||
MedicinesService: class {
|
MedicinesService: class {
|
||||||
list = vi.fn();
|
list = vi.fn();
|
||||||
getById = vi.fn();
|
getById = vi.fn();
|
||||||
|
|
@ -111,7 +111,7 @@ vi.mock('../medicines/medicines.service.js', () => ({
|
||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
|
|
||||||
vi.mock('../cabinet-events/cabinet-events.repository.js', () => ({
|
vi.mock('../../../src/modules/cabinet-events/cabinet-events.repository.js', () => ({
|
||||||
CabinetEventsRepository: class {
|
CabinetEventsRepository: class {
|
||||||
create = vi.fn();
|
create = vi.fn();
|
||||||
createMany = vi.fn();
|
createMany = vi.fn();
|
||||||
|
|
@ -122,7 +122,7 @@ vi.mock('../cabinet-events/cabinet-events.repository.js', () => ({
|
||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
|
|
||||||
vi.mock('../cabinet-events/cabinet-events.service.js', () => ({
|
vi.mock('../../../src/modules/cabinet-events/cabinet-events.service.js', () => ({
|
||||||
CabinetEventsService: class {
|
CabinetEventsService: class {
|
||||||
logEvent = vi.fn();
|
logEvent = vi.fn();
|
||||||
logEvents = vi.fn();
|
logEvents = vi.fn();
|
||||||
|
|
@ -133,20 +133,20 @@ vi.mock('../cabinet-events/cabinet-events.service.js', () => ({
|
||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
|
|
||||||
vi.mock('../users/users.repository.js', () => ({
|
vi.mock('../../../src/modules/users/users.repository.js', () => ({
|
||||||
UsersRepository: class {
|
UsersRepository: class {
|
||||||
findByKeycloakId = vi.fn().mockResolvedValue({ householdIds: ['hh1'] });
|
findByKeycloakId = vi.fn().mockResolvedValue({ householdIds: ['hh1'] });
|
||||||
upsertFromToken = vi.fn().mockResolvedValue({ householdIds: ['hh1'] });
|
upsertFromToken = vi.fn().mockResolvedValue({ householdIds: ['hh1'] });
|
||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
|
|
||||||
import authPlugin from '../../plugins/auth.plugin.js';
|
import authPlugin from '../../../src/plugins/auth.plugin.js';
|
||||||
import householdPlugin from '../../plugins/household.plugin.js';
|
import householdPlugin from '../../../src/plugins/household.plugin.js';
|
||||||
import usersRoutes from '../users/users.routes.js';
|
import usersRoutes from '../../../src/modules/users/users.routes.js';
|
||||||
import medicinesRoutes from '../medicines/medicines.routes.js';
|
import medicinesRoutes from '../../../src/modules/medicines/medicines.routes.js';
|
||||||
import medicineProductsRoutes from '../medicine-products/medicine-products.routes.js';
|
import medicineProductsRoutes from '../../../src/modules/medicine-products/medicine-products.routes.js';
|
||||||
import cabinetEventsRoutes from '../cabinet-events/cabinet-events.routes.js';
|
import cabinetEventsRoutes from '../../../src/modules/cabinet-events/cabinet-events.routes.js';
|
||||||
import cabinetRoutes from './cabinet.routes.js';
|
import cabinetRoutes from '../../../src/modules/cabinet/cabinet.routes.js';
|
||||||
|
|
||||||
function makeFakeCabinetItem(overrides = {}) {
|
function makeFakeCabinetItem(overrides = {}) {
|
||||||
return {
|
return {
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||||
import { CabinetService } from './cabinet.service.js';
|
import { CabinetService } from '../../../src/modules/cabinet/cabinet.service.js';
|
||||||
import { CabinetEventType, CabinetEventSourceType } from '@meshitrack/shared';
|
import { CabinetEventType, CabinetEventSourceType } from '@meshitrack/shared';
|
||||||
|
|
||||||
describe(CabinetService.name, () => {
|
describe(CabinetService.name, () => {
|
||||||
|
|
@ -16,7 +16,7 @@ const {
|
||||||
mockFindById: vi.fn(),
|
mockFindById: vi.fn(),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
vi.mock('../../schemas/freshness-rule.schema.js', () => {
|
vi.mock('../../../src/schemas/freshness-rule.schema.js', () => {
|
||||||
const chain = () => ({
|
const chain = () => ({
|
||||||
sort: vi.fn().mockReturnThis(),
|
sort: vi.fn().mockReturnThis(),
|
||||||
limit: vi.fn().mockReturnThis(),
|
limit: vi.fn().mockReturnThis(),
|
||||||
|
|
@ -62,7 +62,7 @@ vi.mock('../../schemas/freshness-rule.schema.js', () => {
|
||||||
return { FreshnessRuleModel: FakeModel };
|
return { FreshnessRuleModel: FakeModel };
|
||||||
});
|
});
|
||||||
|
|
||||||
import { FreshnessRulesRepository } from './freshness-rules.repository.js';
|
import { FreshnessRulesRepository } from '../../../src/modules/freshness-rules/freshness-rules.repository.js';
|
||||||
|
|
||||||
describe(FreshnessRulesRepository.name, () => {
|
describe(FreshnessRulesRepository.name, () => {
|
||||||
let repo: FreshnessRulesRepository;
|
let repo: FreshnessRulesRepository;
|
||||||
|
|
@ -28,7 +28,7 @@ const { mockFindByHousehold, mockFindById, mockCreate, mockUpdate, mockDelete }
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
vi.mock('./freshness-rules.repository.js', () => ({
|
vi.mock('../../../src/modules/freshness-rules/freshness-rules.repository.js', () => ({
|
||||||
FreshnessRulesRepository: class {
|
FreshnessRulesRepository: class {
|
||||||
findByHousehold = mockFindByHousehold;
|
findByHousehold = mockFindByHousehold;
|
||||||
findById = mockFindById;
|
findById = mockFindById;
|
||||||
|
|
@ -39,17 +39,17 @@ vi.mock('./freshness-rules.repository.js', () => ({
|
||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
|
|
||||||
vi.mock('../users/users.repository.js', () => ({
|
vi.mock('../../../src/modules/users/users.repository.js', () => ({
|
||||||
UsersRepository: class {
|
UsersRepository: class {
|
||||||
findByKeycloakId = vi.fn().mockResolvedValue({ householdIds: ['hh1'] });
|
findByKeycloakId = vi.fn().mockResolvedValue({ householdIds: ['hh1'] });
|
||||||
upsertFromToken = vi.fn().mockResolvedValue({ householdIds: ['hh1'] });
|
upsertFromToken = vi.fn().mockResolvedValue({ householdIds: ['hh1'] });
|
||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
|
|
||||||
import authPlugin from '../../plugins/auth.plugin.js';
|
import authPlugin from '../../../src/plugins/auth.plugin.js';
|
||||||
import householdPlugin from '../../plugins/household.plugin.js';
|
import householdPlugin from '../../../src/plugins/household.plugin.js';
|
||||||
import usersRoutes from '../users/users.routes.js';
|
import usersRoutes from '../../../src/modules/users/users.routes.js';
|
||||||
import freshnessRulesRoutes from './freshness-rules.routes.js';
|
import freshnessRulesRoutes from '../../../src/modules/freshness-rules/freshness-rules.routes.js';
|
||||||
|
|
||||||
function makeRule(overrides: Record<string, unknown> = {}) {
|
function makeRule(overrides: Record<string, unknown> = {}) {
|
||||||
return {
|
return {
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||||
import { FreshnessRulesService } from './freshness-rules.service.js';
|
import { FreshnessRulesService } from '../../../src/modules/freshness-rules/freshness-rules.service.js';
|
||||||
import { NotFoundError, BadRequestError } from '../../common/errors.js';
|
import { NotFoundError, BadRequestError } from '../../../src/common/errors.js';
|
||||||
import { FreshnessRuleSource } from '@meshitrack/shared';
|
import { FreshnessRuleSource } from '@meshitrack/shared';
|
||||||
|
|
||||||
const mockRepo = {
|
const mockRepo = {
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
import { describe, it, expect } from 'vitest';
|
import { describe, it, expect } from 'vitest';
|
||||||
import Fastify from 'fastify';
|
import Fastify from 'fastify';
|
||||||
import { serializerCompiler, validatorCompiler } from 'fastify-type-provider-zod';
|
import { serializerCompiler, validatorCompiler } from 'fastify-type-provider-zod';
|
||||||
import healthRoutes from './health.routes.js';
|
import healthRoutes from '../../../src/modules/health/health.routes.js';
|
||||||
|
|
||||||
describe('Health Routes', () => {
|
describe('Health Routes', () => {
|
||||||
async function buildTestApp() {
|
async function buildTestApp() {
|
||||||
|
|
@ -15,7 +15,7 @@ const { _mockLean, mockExec, mockFindById, mockFindOne, mockFindByIdAndUpdate, m
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
vi.mock('../../schemas/household.schema.js', () => {
|
vi.mock('../../../src/schemas/household.schema.js', () => {
|
||||||
class MockHouseholdModel {
|
class MockHouseholdModel {
|
||||||
_data: Record<string, unknown>;
|
_data: Record<string, unknown>;
|
||||||
constructor(data: Record<string, unknown>) {
|
constructor(data: Record<string, unknown>) {
|
||||||
|
|
@ -36,7 +36,7 @@ vi.mock('../../schemas/household.schema.js', () => {
|
||||||
return { HouseholdModel: MockHouseholdModel };
|
return { HouseholdModel: MockHouseholdModel };
|
||||||
});
|
});
|
||||||
|
|
||||||
import { HouseholdsRepository } from './households.repository.js';
|
import { HouseholdsRepository } from '../../../src/modules/households/households.repository.js';
|
||||||
|
|
||||||
describe('HouseholdsRepository', () => {
|
describe('HouseholdsRepository', () => {
|
||||||
let repo: HouseholdsRepository;
|
let repo: HouseholdsRepository;
|
||||||
|
|
@ -41,7 +41,7 @@ const {
|
||||||
mockUserUpdate: vi.fn(),
|
mockUserUpdate: vi.fn(),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
vi.mock('./households.repository.js', () => ({
|
vi.mock('../../../src/modules/households/households.repository.js', () => ({
|
||||||
HouseholdsRepository: class {
|
HouseholdsRepository: class {
|
||||||
create = mockCreate;
|
create = mockCreate;
|
||||||
findById = mockFindById;
|
findById = mockFindById;
|
||||||
|
|
@ -52,7 +52,7 @@ vi.mock('./households.repository.js', () => ({
|
||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
|
|
||||||
vi.mock('../users/users.repository.js', () => ({
|
vi.mock('../../../src/modules/users/users.repository.js', () => ({
|
||||||
UsersRepository: class {
|
UsersRepository: class {
|
||||||
findByKeycloakId = mockFindByKeycloakId;
|
findByKeycloakId = mockFindByKeycloakId;
|
||||||
update = mockUserUpdate;
|
update = mockUserUpdate;
|
||||||
|
|
@ -75,10 +75,10 @@ vi.mock('mongoose', () => ({
|
||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
|
|
||||||
import authPlugin from '../../plugins/auth.plugin.js';
|
import authPlugin from '../../../src/plugins/auth.plugin.js';
|
||||||
import householdPlugin from '../../plugins/household.plugin.js';
|
import householdPlugin from '../../../src/plugins/household.plugin.js';
|
||||||
import usersRoutes from '../users/users.routes.js';
|
import usersRoutes from '../../../src/modules/users/users.routes.js';
|
||||||
import householdsRoutes from './households.routes.js';
|
import householdsRoutes from '../../../src/modules/households/households.routes.js';
|
||||||
|
|
||||||
function makeFakeHousehold(overrides = {}) {
|
function makeFakeHousehold(overrides = {}) {
|
||||||
return {
|
return {
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||||
import { HouseholdsService } from './households.service.js';
|
import { HouseholdsService } from '../../../src/modules/households/households.service.js';
|
||||||
import { NotFoundError, ForbiddenError, ConflictError } from '../../common/errors.js';
|
import { NotFoundError, ForbiddenError, ConflictError } from '../../../src/common/errors.js';
|
||||||
import { HouseholdRole } from '@meshitrack/shared';
|
import { HouseholdRole } from '@meshitrack/shared';
|
||||||
|
|
||||||
// Mock uuid to return deterministic values
|
// Mock uuid to return deterministic values
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import { describe, it, expect, vi } from 'vitest';
|
import { describe, it, expect, vi } from 'vitest';
|
||||||
import { NoOpLlmProvider } from './no-op-llm.provider.js';
|
import { NoOpLlmProvider } from '../../../src/modules/llm/no-op-llm.provider.js';
|
||||||
import { LLM_PROVIDER } from './llm-provider.interface.js';
|
import { LLM_PROVIDER } from '../../../src/modules/llm/llm-provider.interface.js';
|
||||||
|
|
||||||
describe(NoOpLlmProvider.name, () => {
|
describe(NoOpLlmProvider.name, () => {
|
||||||
const provider = new NoOpLlmProvider();
|
const provider = new NoOpLlmProvider();
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||||
import { MealPlanRepository } from './meal-plans.repository.js';
|
import { MealPlanRepository } from '../../../src/modules/meal-plans/meal-plans.repository.js';
|
||||||
import { MealPlanStatus } from '@meshitrack/shared';
|
import { MealPlanStatus } from '@meshitrack/shared';
|
||||||
|
|
||||||
const { mockSave, MockMealPlanModel } = vi.hoisted(() => {
|
const { mockSave, MockMealPlanModel } = vi.hoisted(() => {
|
||||||
|
|
@ -17,11 +17,11 @@ const { mockSave, MockMealPlanModel } = vi.hoisted(() => {
|
||||||
return { mockSave, MockMealPlanModel: MockModel };
|
return { mockSave, MockMealPlanModel: MockModel };
|
||||||
});
|
});
|
||||||
|
|
||||||
vi.mock('../../schemas/meal-plan.schema.js', () => ({
|
vi.mock('../../../src/schemas/meal-plan.schema.js', () => ({
|
||||||
MealPlanModel: MockMealPlanModel,
|
MealPlanModel: MockMealPlanModel,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const { MealPlanModel } = await import('../../schemas/meal-plan.schema.js');
|
const { MealPlanModel } = await import('../../../src/schemas/meal-plan.schema.js');
|
||||||
|
|
||||||
function makeChain(result: unknown = null) {
|
function makeChain(result: unknown = null) {
|
||||||
return {
|
return {
|
||||||
|
|
@ -37,7 +37,7 @@ const {
|
||||||
mockDelete: vi.fn(),
|
mockDelete: vi.fn(),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
vi.mock('./meal-plans.repository.js', () => ({
|
vi.mock('../../../src/modules/meal-plans/meal-plans.repository.js', () => ({
|
||||||
MealPlanRepository: class {
|
MealPlanRepository: class {
|
||||||
findByHousehold = mockFindByHousehold;
|
findByHousehold = mockFindByHousehold;
|
||||||
findById = mockFindById;
|
findById = mockFindById;
|
||||||
|
|
@ -50,42 +50,42 @@ vi.mock('./meal-plans.repository.js', () => ({
|
||||||
}));
|
}));
|
||||||
|
|
||||||
// Mock prerequisite repositories to allow SuggestionEngine/Gap to resolve
|
// Mock prerequisite repositories to allow SuggestionEngine/Gap to resolve
|
||||||
vi.mock('../recipes/recipes.repository.js', () => ({
|
vi.mock('../../../src/modules/recipes/recipes.repository.js', () => ({
|
||||||
RecipesRepository: class {
|
RecipesRepository: class {
|
||||||
findByHousehold = vi.fn().mockResolvedValue({ data: [] });
|
findByHousehold = vi.fn().mockResolvedValue({ data: [] });
|
||||||
findById = vi.fn();
|
findById = vi.fn();
|
||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
|
|
||||||
vi.mock('../pantry/pantry.repository.js', () => ({
|
vi.mock('../../../src/modules/pantry/pantry.repository.js', () => ({
|
||||||
PantryRepository: class {
|
PantryRepository: class {
|
||||||
findActiveByHousehold = vi.fn().mockResolvedValue([]);
|
findActiveByHousehold = vi.fn().mockResolvedValue([]);
|
||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
|
|
||||||
vi.mock('../nutrition-targets/nutrition-target.repository.js', () => ({
|
vi.mock('../../../src/modules/nutrition-targets/nutrition-target.repository.js', () => ({
|
||||||
NutritionTargetRepository: class {
|
NutritionTargetRepository: class {
|
||||||
findByUser = vi.fn().mockResolvedValue(null);
|
findByUser = vi.fn().mockResolvedValue(null);
|
||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
|
|
||||||
vi.mock('../products/products.repository.js', () => ({
|
vi.mock('../../../src/modules/products/products.repository.js', () => ({
|
||||||
ProductsRepository: class {
|
ProductsRepository: class {
|
||||||
findByIds = vi.fn().mockResolvedValue([]);
|
findByIds = vi.fn().mockResolvedValue([]);
|
||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
|
|
||||||
vi.mock('../users/users.repository.js', () => ({
|
vi.mock('../../../src/modules/users/users.repository.js', () => ({
|
||||||
UsersRepository: class {
|
UsersRepository: class {
|
||||||
findByKeycloakId = vi.fn().mockResolvedValue({ householdIds: ['hh1'] });
|
findByKeycloakId = vi.fn().mockResolvedValue({ householdIds: ['hh1'] });
|
||||||
upsertFromToken = vi.fn().mockResolvedValue({ householdIds: ['hh1'] });
|
upsertFromToken = vi.fn().mockResolvedValue({ householdIds: ['hh1'] });
|
||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
|
|
||||||
import authPlugin from '../../plugins/auth.plugin.js';
|
import authPlugin from '../../../src/plugins/auth.plugin.js';
|
||||||
import householdPlugin from '../../plugins/household.plugin.js';
|
import householdPlugin from '../../../src/plugins/household.plugin.js';
|
||||||
import usersRoutes from '../users/users.routes.js';
|
import usersRoutes from '../../../src/modules/users/users.routes.js';
|
||||||
import mealPlanRoutes from './meal-plans.routes.js';
|
import mealPlanRoutes from '../../../src/modules/meal-plans/meal-plans.routes.js';
|
||||||
|
|
||||||
const emptyNutrition = { calories: 0, protein: 0, carbs: 0, fat: 0, fiber: 0, sugar: 0, sodium: 0, saturatedFat: 0, cholesterol: 0 };
|
const emptyNutrition = { calories: 0, protein: 0, carbs: 0, fat: 0, fiber: 0, sugar: 0, sodium: 0, saturatedFat: 0, cholesterol: 0 };
|
||||||
|
|
||||||
|
|
@ -1,8 +1,8 @@
|
||||||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||||
import { MealPlanService } from './meal-plans.service.js';
|
import { MealPlanService } from '../../../src/modules/meal-plans/meal-plans.service.js';
|
||||||
import type { MealPlanRepository } from './meal-plans.repository.js';
|
import type { MealPlanRepository } from '../../../src/modules/meal-plans/meal-plans.repository.js';
|
||||||
import { MealPlanStatus, MealType } from '@meshitrack/shared';
|
import { MealPlanStatus, MealType } from '@meshitrack/shared';
|
||||||
import { BadRequestError, NotFoundError } from '../../common/errors.js';
|
import { BadRequestError, NotFoundError } from '../../../src/common/errors.js';
|
||||||
|
|
||||||
describe(MealPlanService.name, () => {
|
describe(MealPlanService.name, () => {
|
||||||
let service: MealPlanService;
|
let service: MealPlanService;
|
||||||
|
|
@ -1,10 +1,10 @@
|
||||||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||||
import { ShoppingGapService } from './shopping-gap.service.js';
|
import { ShoppingGapService } from '../../../src/modules/meal-plans/shopping-gap.service.js';
|
||||||
import type { MealPlanRepository } from './meal-plans.repository.js';
|
import type { MealPlanRepository } from '../../../src/modules/meal-plans/meal-plans.repository.js';
|
||||||
import type { RecipesRepository } from '../recipes/recipes.repository.js';
|
import type { RecipesRepository } from '../../../src/modules/recipes/recipes.repository.js';
|
||||||
import type { PantryRepository } from '../pantry/pantry.repository.js';
|
import type { PantryRepository } from '../../../src/modules/pantry/pantry.repository.js';
|
||||||
import type { ProductsRepository } from '../products/products.repository.js';
|
import type { ProductsRepository } from '../../../src/modules/products/products.repository.js';
|
||||||
import { NotFoundError } from '../../common/errors.js';
|
import { NotFoundError } from '../../../src/common/errors.js';
|
||||||
|
|
||||||
describe(ShoppingGapService.name, () => {
|
describe(ShoppingGapService.name, () => {
|
||||||
let service: ShoppingGapService;
|
let service: ShoppingGapService;
|
||||||
|
|
@ -1,9 +1,9 @@
|
||||||
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
||||||
import { SuggestionEngineService } from './suggestion-engine.service.js';
|
import { SuggestionEngineService } from '../../../src/modules/meal-plans/suggestion-engine.service.js';
|
||||||
import type { RecipesRepository } from '../recipes/recipes.repository.js';
|
import type { RecipesRepository } from '../../../src/modules/recipes/recipes.repository.js';
|
||||||
import type { PantryRepository } from '../pantry/pantry.repository.js';
|
import type { PantryRepository } from '../../../src/modules/pantry/pantry.repository.js';
|
||||||
import type { MealPlanRepository } from './meal-plans.repository.js';
|
import type { MealPlanRepository } from '../../../src/modules/meal-plans/meal-plans.repository.js';
|
||||||
import type { NutritionTargetRepository } from '../nutrition-targets/nutrition-target.repository.js';
|
import type { NutritionTargetRepository } from '../../../src/modules/nutrition-targets/nutrition-target.repository.js';
|
||||||
|
|
||||||
describe(SuggestionEngineService.name, () => {
|
describe(SuggestionEngineService.name, () => {
|
||||||
let service: SuggestionEngineService;
|
let service: SuggestionEngineService;
|
||||||
|
|
@ -7,7 +7,7 @@ const { mockFind, mockFindOne, mockAggregate, mockSave } = vi.hoisted(() => ({
|
||||||
mockSave: vi.fn(),
|
mockSave: vi.fn(),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
vi.mock('../../schemas/medicine-price.schema.js', () => {
|
vi.mock('../../../src/schemas/medicine-price.schema.js', () => {
|
||||||
const chain = () => ({
|
const chain = () => ({
|
||||||
sort: vi.fn().mockReturnThis(),
|
sort: vi.fn().mockReturnThis(),
|
||||||
limit: vi.fn().mockReturnThis(),
|
limit: vi.fn().mockReturnThis(),
|
||||||
|
|
@ -37,7 +37,7 @@ vi.mock('../../schemas/medicine-price.schema.js', () => {
|
||||||
return { MedicinePriceModel: FakeModel };
|
return { MedicinePriceModel: FakeModel };
|
||||||
});
|
});
|
||||||
|
|
||||||
import { MedicinePricesRepository } from './medicine-prices.repository.js';
|
import { MedicinePricesRepository } from '../../../src/modules/medicine-prices/medicine-prices.repository.js';
|
||||||
|
|
||||||
describe(MedicinePricesRepository.name, () => {
|
describe(MedicinePricesRepository.name, () => {
|
||||||
let repo: MedicinePricesRepository;
|
let repo: MedicinePricesRepository;
|
||||||
|
|
@ -27,7 +27,7 @@ const { mockRecordPrice, mockGetPriceHistory, mockCompareStores, mockGetAnalytic
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
vi.mock('./medicine-prices.repository.js', () => ({
|
vi.mock('../../../src/modules/medicine-prices/medicine-prices.repository.js', () => ({
|
||||||
MedicinePricesRepository: class {
|
MedicinePricesRepository: class {
|
||||||
create = vi.fn();
|
create = vi.fn();
|
||||||
findByMedicine = vi.fn();
|
findByMedicine = vi.fn();
|
||||||
|
|
@ -37,7 +37,7 @@ vi.mock('./medicine-prices.repository.js', () => ({
|
||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
|
|
||||||
vi.mock('./medicine-prices.service.js', () => ({
|
vi.mock('../../../src/modules/medicine-prices/medicine-prices.service.js', () => ({
|
||||||
MedicinePricesService: class {
|
MedicinePricesService: class {
|
||||||
recordPrice = mockRecordPrice;
|
recordPrice = mockRecordPrice;
|
||||||
getPriceHistory = mockGetPriceHistory;
|
getPriceHistory = mockGetPriceHistory;
|
||||||
|
|
@ -46,29 +46,29 @@ vi.mock('./medicine-prices.service.js', () => ({
|
||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
|
|
||||||
vi.mock('../medicine-products/medicine-products.repository.js', () => ({
|
vi.mock('../../../src/modules/medicine-products/medicine-products.repository.js', () => ({
|
||||||
MedicineProductsRepository: class {
|
MedicineProductsRepository: class {
|
||||||
findById = vi.fn();
|
findById = vi.fn();
|
||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
|
|
||||||
vi.mock('../stores/stores.repository.js', () => ({
|
vi.mock('../../../src/modules/stores/stores.repository.js', () => ({
|
||||||
StoresRepository: class {
|
StoresRepository: class {
|
||||||
findById = vi.fn();
|
findById = vi.fn();
|
||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
|
|
||||||
vi.mock('../users/users.repository.js', () => ({
|
vi.mock('../../../src/modules/users/users.repository.js', () => ({
|
||||||
UsersRepository: class {
|
UsersRepository: class {
|
||||||
findByKeycloakId = vi.fn().mockResolvedValue({ householdIds: ['hh1'] });
|
findByKeycloakId = vi.fn().mockResolvedValue({ householdIds: ['hh1'] });
|
||||||
upsertFromToken = vi.fn().mockResolvedValue({ householdIds: ['hh1'] });
|
upsertFromToken = vi.fn().mockResolvedValue({ householdIds: ['hh1'] });
|
||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
|
|
||||||
import authPlugin from '../../plugins/auth.plugin.js';
|
import authPlugin from '../../../src/plugins/auth.plugin.js';
|
||||||
import householdPlugin from '../../plugins/household.plugin.js';
|
import householdPlugin from '../../../src/plugins/household.plugin.js';
|
||||||
import usersRoutes from '../users/users.routes.js';
|
import usersRoutes from '../../../src/modules/users/users.routes.js';
|
||||||
import medicinePricesRoutes from './medicine-prices.routes.js';
|
import medicinePricesRoutes from '../../../src/modules/medicine-prices/medicine-prices.routes.js';
|
||||||
|
|
||||||
function makeFakePriceRecord(overrides = {}) {
|
function makeFakePriceRecord(overrides = {}) {
|
||||||
return {
|
return {
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||||
import { MedicinePricesService } from './medicine-prices.service.js';
|
import { MedicinePricesService } from '../../../src/modules/medicine-prices/medicine-prices.service.js';
|
||||||
|
|
||||||
describe(MedicinePricesService.name, () => {
|
describe(MedicinePricesService.name, () => {
|
||||||
const mockPricesRepo = {
|
const mockPricesRepo = {
|
||||||
|
|
@ -30,7 +30,7 @@ const {
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
vi.mock('../../schemas/medicine-product.schema.js', () => {
|
vi.mock('../../../src/schemas/medicine-product.schema.js', () => {
|
||||||
class MockMedicineProductModel {
|
class MockMedicineProductModel {
|
||||||
_data: Record<string, unknown>;
|
_data: Record<string, unknown>;
|
||||||
constructor(data: Record<string, unknown>) {
|
constructor(data: Record<string, unknown>) {
|
||||||
|
|
@ -52,7 +52,7 @@ vi.mock('../../schemas/medicine-product.schema.js', () => {
|
||||||
return { MedicineProductModel: MockMedicineProductModel };
|
return { MedicineProductModel: MockMedicineProductModel };
|
||||||
});
|
});
|
||||||
|
|
||||||
import { MedicineProductsRepository } from './medicine-products.repository.js';
|
import { MedicineProductsRepository } from '../../../src/modules/medicine-products/medicine-products.repository.js';
|
||||||
|
|
||||||
describe(MedicineProductsRepository.name, () => {
|
describe(MedicineProductsRepository.name, () => {
|
||||||
let repo: MedicineProductsRepository;
|
let repo: MedicineProductsRepository;
|
||||||
|
|
@ -35,7 +35,7 @@ const {
|
||||||
mockMedicineFindById: vi.fn(),
|
mockMedicineFindById: vi.fn(),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
vi.mock('./medicine-products.repository.js', () => ({
|
vi.mock('../../../src/modules/medicine-products/medicine-products.repository.js', () => ({
|
||||||
MedicineProductsRepository: class {
|
MedicineProductsRepository: class {
|
||||||
findByMedicine = mockFindByMedicine;
|
findByMedicine = mockFindByMedicine;
|
||||||
findById = mockFindById;
|
findById = mockFindById;
|
||||||
|
|
@ -45,24 +45,24 @@ vi.mock('./medicine-products.repository.js', () => ({
|
||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
|
|
||||||
vi.mock('../medicines/medicines.repository.js', () => ({
|
vi.mock('../../../src/modules/medicines/medicines.repository.js', () => ({
|
||||||
MedicinesRepository: class {
|
MedicinesRepository: class {
|
||||||
findById = mockMedicineFindById;
|
findById = mockMedicineFindById;
|
||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
|
|
||||||
vi.mock('../users/users.repository.js', () => ({
|
vi.mock('../../../src/modules/users/users.repository.js', () => ({
|
||||||
UsersRepository: class {
|
UsersRepository: class {
|
||||||
findByKeycloakId = vi.fn().mockResolvedValue({ householdIds: ['hh1'] });
|
findByKeycloakId = vi.fn().mockResolvedValue({ householdIds: ['hh1'] });
|
||||||
upsertFromToken = vi.fn().mockResolvedValue({ householdIds: ['hh1'] });
|
upsertFromToken = vi.fn().mockResolvedValue({ householdIds: ['hh1'] });
|
||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
|
|
||||||
import authPlugin from '../../plugins/auth.plugin.js';
|
import authPlugin from '../../../src/plugins/auth.plugin.js';
|
||||||
import householdPlugin from '../../plugins/household.plugin.js';
|
import householdPlugin from '../../../src/plugins/household.plugin.js';
|
||||||
import usersRoutes from '../users/users.routes.js';
|
import usersRoutes from '../../../src/modules/users/users.routes.js';
|
||||||
import medicinesRoutes from '../medicines/medicines.routes.js';
|
import medicinesRoutes from '../../../src/modules/medicines/medicines.routes.js';
|
||||||
import medicineProductsRoutes from './medicine-products.routes.js';
|
import medicineProductsRoutes from '../../../src/modules/medicine-products/medicine-products.routes.js';
|
||||||
|
|
||||||
function makeFakeProduct(overrides = {}) {
|
function makeFakeProduct(overrides = {}) {
|
||||||
return {
|
return {
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||||
import { MedicineProductsService } from './medicine-products.service.js';
|
import { MedicineProductsService } from '../../../src/modules/medicine-products/medicine-products.service.js';
|
||||||
import { NotFoundError } from '../../common/errors.js';
|
import { NotFoundError } from '../../../src/common/errors.js';
|
||||||
import { DosageUnit, MedicineProductSource } from '@meshitrack/shared';
|
import { DosageUnit, MedicineProductSource } from '@meshitrack/shared';
|
||||||
|
|
||||||
describe(MedicineProductsService.name, () => {
|
describe(MedicineProductsService.name, () => {
|
||||||
|
|
@ -27,7 +27,7 @@ const {
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
vi.mock('../../schemas/medicine.schema.js', () => {
|
vi.mock('../../../src/schemas/medicine.schema.js', () => {
|
||||||
class MockMedicineModel {
|
class MockMedicineModel {
|
||||||
_data: Record<string, unknown>;
|
_data: Record<string, unknown>;
|
||||||
constructor(data: Record<string, unknown>) {
|
constructor(data: Record<string, unknown>) {
|
||||||
|
|
@ -48,7 +48,7 @@ vi.mock('../../schemas/medicine.schema.js', () => {
|
||||||
return { MedicineModel: MockMedicineModel };
|
return { MedicineModel: MockMedicineModel };
|
||||||
});
|
});
|
||||||
|
|
||||||
import { MedicinesRepository } from './medicines.repository.js';
|
import { MedicinesRepository } from '../../../src/modules/medicines/medicines.repository.js';
|
||||||
|
|
||||||
describe(MedicinesRepository.name, () => {
|
describe(MedicinesRepository.name, () => {
|
||||||
let repo: MedicinesRepository;
|
let repo: MedicinesRepository;
|
||||||
|
|
@ -37,7 +37,7 @@ const {
|
||||||
mockCountByMedicineId: vi.fn(),
|
mockCountByMedicineId: vi.fn(),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
vi.mock('./medicines.repository.js', () => ({
|
vi.mock('../../../src/modules/medicines/medicines.repository.js', () => ({
|
||||||
MedicinesRepository: class {
|
MedicinesRepository: class {
|
||||||
findByHousehold = mockFindByHousehold;
|
findByHousehold = mockFindByHousehold;
|
||||||
findById = mockFindById;
|
findById = mockFindById;
|
||||||
|
|
@ -48,13 +48,13 @@ vi.mock('./medicines.repository.js', () => ({
|
||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
|
|
||||||
vi.mock('../medicine-products/medicine-products.repository.js', () => ({
|
vi.mock('../../../src/modules/medicine-products/medicine-products.repository.js', () => ({
|
||||||
MedicineProductsRepository: class {
|
MedicineProductsRepository: class {
|
||||||
countByMedicineId = mockCountByMedicineId;
|
countByMedicineId = mockCountByMedicineId;
|
||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
|
|
||||||
vi.mock('../medicine-products/medicine-products.service.js', () => ({
|
vi.mock('../../../src/modules/medicine-products/medicine-products.service.js', () => ({
|
||||||
MedicineProductsService: class {
|
MedicineProductsService: class {
|
||||||
listByMedicine = vi.fn();
|
listByMedicine = vi.fn();
|
||||||
getById = vi.fn();
|
getById = vi.fn();
|
||||||
|
|
@ -64,18 +64,18 @@ vi.mock('../medicine-products/medicine-products.service.js', () => ({
|
||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
|
|
||||||
vi.mock('../users/users.repository.js', () => ({
|
vi.mock('../../../src/modules/users/users.repository.js', () => ({
|
||||||
UsersRepository: class {
|
UsersRepository: class {
|
||||||
findByKeycloakId = vi.fn().mockResolvedValue({ householdIds: ['hh1'] });
|
findByKeycloakId = vi.fn().mockResolvedValue({ householdIds: ['hh1'] });
|
||||||
upsertFromToken = vi.fn().mockResolvedValue({ householdIds: ['hh1'] });
|
upsertFromToken = vi.fn().mockResolvedValue({ householdIds: ['hh1'] });
|
||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
|
|
||||||
import authPlugin from '../../plugins/auth.plugin.js';
|
import authPlugin from '../../../src/plugins/auth.plugin.js';
|
||||||
import householdPlugin from '../../plugins/household.plugin.js';
|
import householdPlugin from '../../../src/plugins/household.plugin.js';
|
||||||
import usersRoutes from '../users/users.routes.js';
|
import usersRoutes from '../../../src/modules/users/users.routes.js';
|
||||||
import medicinesRoutes from './medicines.routes.js';
|
import medicinesRoutes from '../../../src/modules/medicines/medicines.routes.js';
|
||||||
import medicineProductsRoutes from '../medicine-products/medicine-products.routes.js';
|
import medicineProductsRoutes from '../../../src/modules/medicine-products/medicine-products.routes.js';
|
||||||
|
|
||||||
function makeFakeMedicine(overrides = {}) {
|
function makeFakeMedicine(overrides = {}) {
|
||||||
return {
|
return {
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||||
import { MedicinesService } from './medicines.service.js';
|
import { MedicinesService } from '../../../src/modules/medicines/medicines.service.js';
|
||||||
import { NotFoundError, ConflictError } from '../../common/errors.js';
|
import { NotFoundError, ConflictError } from '../../../src/common/errors.js';
|
||||||
import { MedicineForm, StrengthUnit, MedicineCategory } from '@meshitrack/shared';
|
import { MedicineForm, StrengthUnit, MedicineCategory } from '@meshitrack/shared';
|
||||||
|
|
||||||
describe(MedicinesService.name, () => {
|
describe(MedicinesService.name, () => {
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||||
import { NutritionTargetRepository } from './nutrition-target.repository.js';
|
import { NutritionTargetRepository } from '../../../src/modules/nutrition-targets/nutrition-target.repository.js';
|
||||||
|
|
||||||
const { mockSave, MockTargetModel } = vi.hoisted(() => {
|
const { mockSave, MockTargetModel } = vi.hoisted(() => {
|
||||||
const mockSave = vi.fn();
|
const mockSave = vi.fn();
|
||||||
|
|
@ -16,11 +16,11 @@ const { mockSave, MockTargetModel } = vi.hoisted(() => {
|
||||||
return { mockSave, MockTargetModel: MockModel };
|
return { mockSave, MockTargetModel: MockModel };
|
||||||
});
|
});
|
||||||
|
|
||||||
vi.mock('../../schemas/nutrition-target.schema.js', () => ({
|
vi.mock('../../../src/schemas/nutrition-target.schema.js', () => ({
|
||||||
NutritionTargetModel: MockTargetModel,
|
NutritionTargetModel: MockTargetModel,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const { NutritionTargetModel } = await import('../../schemas/nutrition-target.schema.js');
|
const { NutritionTargetModel } = await import('../../../src/schemas/nutrition-target.schema.js');
|
||||||
|
|
||||||
function makeChain(result: unknown = null) {
|
function makeChain(result: unknown = null) {
|
||||||
return {
|
return {
|
||||||
|
|
@ -30,7 +30,7 @@ const {
|
||||||
mockCreate: vi.fn(),
|
mockCreate: vi.fn(),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
vi.mock('./nutrition-target.repository.js', () => ({
|
vi.mock('../../../src/modules/nutrition-targets/nutrition-target.repository.js', () => ({
|
||||||
NutritionTargetRepository: class {
|
NutritionTargetRepository: class {
|
||||||
findByUser = mockFindByUser;
|
findByUser = mockFindByUser;
|
||||||
findAllByUser = mockFindAllByUser;
|
findAllByUser = mockFindAllByUser;
|
||||||
|
|
@ -39,17 +39,17 @@ vi.mock('./nutrition-target.repository.js', () => ({
|
||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
|
|
||||||
vi.mock('../users/users.repository.js', () => ({
|
vi.mock('../../../src/modules/users/users.repository.js', () => ({
|
||||||
UsersRepository: class {
|
UsersRepository: class {
|
||||||
findByKeycloakId = vi.fn().mockResolvedValue({ householdIds: ['hh1'] });
|
findByKeycloakId = vi.fn().mockResolvedValue({ householdIds: ['hh1'] });
|
||||||
upsertFromToken = vi.fn().mockResolvedValue({ householdIds: ['hh1'] });
|
upsertFromToken = vi.fn().mockResolvedValue({ householdIds: ['hh1'] });
|
||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
|
|
||||||
import authPlugin from '../../plugins/auth.plugin.js';
|
import authPlugin from '../../../src/plugins/auth.plugin.js';
|
||||||
import householdPlugin from '../../plugins/household.plugin.js';
|
import householdPlugin from '../../../src/plugins/household.plugin.js';
|
||||||
import usersRoutes from '../users/users.routes.js';
|
import usersRoutes from '../../../src/modules/users/users.routes.js';
|
||||||
import nutritionTargetRoutes from './nutrition-target.routes.js';
|
import nutritionTargetRoutes from '../../../src/modules/nutrition-targets/nutrition-target.routes.js';
|
||||||
|
|
||||||
function makeTarget(overrides = {}) {
|
function makeTarget(overrides = {}) {
|
||||||
return {
|
return {
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||||
import { NutritionTargetService } from './nutrition-target.service.js';
|
import { NutritionTargetService } from '../../../src/modules/nutrition-targets/nutrition-target.service.js';
|
||||||
import type { NutritionTargetRepository } from './nutrition-target.repository.js';
|
import type { NutritionTargetRepository } from '../../../src/modules/nutrition-targets/nutrition-target.repository.js';
|
||||||
|
|
||||||
describe(NutritionTargetService.name, () => {
|
describe(NutritionTargetService.name, () => {
|
||||||
let service: NutritionTargetService;
|
let service: NutritionTargetService;
|
||||||
|
|
@ -7,7 +7,7 @@ const { mockFind, mockFindOne, mockFindOneAndUpdate, mockSave } = vi.hoisted(()
|
||||||
mockSave: vi.fn(),
|
mockSave: vi.fn(),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
vi.mock('../../schemas/organizer-fill.schema.js', () => {
|
vi.mock('../../../src/schemas/organizer-fill.schema.js', () => {
|
||||||
const chain = () => ({
|
const chain = () => ({
|
||||||
sort: vi.fn().mockReturnThis(),
|
sort: vi.fn().mockReturnThis(),
|
||||||
limit: vi.fn().mockReturnThis(),
|
limit: vi.fn().mockReturnThis(),
|
||||||
|
|
@ -41,7 +41,7 @@ vi.mock('../../schemas/organizer-fill.schema.js', () => {
|
||||||
return { OrganizerFillModel: FakeModel };
|
return { OrganizerFillModel: FakeModel };
|
||||||
});
|
});
|
||||||
|
|
||||||
import { OrganizerRepository } from './organizer.repository.js';
|
import { OrganizerRepository } from '../../../src/modules/organizer/organizer.repository.js';
|
||||||
|
|
||||||
describe(OrganizerRepository.name, () => {
|
describe(OrganizerRepository.name, () => {
|
||||||
let repo: OrganizerRepository;
|
let repo: OrganizerRepository;
|
||||||
|
|
@ -27,7 +27,7 @@ const { mockListFills, mockGetFillById, mockPreview, mockFill, mockUndoFill } =
|
||||||
mockUndoFill: vi.fn(),
|
mockUndoFill: vi.fn(),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
vi.mock('./organizer.repository.js', () => ({
|
vi.mock('../../../src/modules/organizer/organizer.repository.js', () => ({
|
||||||
OrganizerRepository: class {
|
OrganizerRepository: class {
|
||||||
create = vi.fn();
|
create = vi.fn();
|
||||||
update = vi.fn();
|
update = vi.fn();
|
||||||
|
|
@ -37,7 +37,7 @@ vi.mock('./organizer.repository.js', () => ({
|
||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
|
|
||||||
vi.mock('./organizer.service.js', () => ({
|
vi.mock('../../../src/modules/organizer/organizer.service.js', () => ({
|
||||||
OrganizerService: class {
|
OrganizerService: class {
|
||||||
listFills = mockListFills;
|
listFills = mockListFills;
|
||||||
getFillById = mockGetFillById;
|
getFillById = mockGetFillById;
|
||||||
|
|
@ -47,17 +47,17 @@ vi.mock('./organizer.service.js', () => ({
|
||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
|
|
||||||
vi.mock('../users/users.repository.js', () => ({
|
vi.mock('../../../src/modules/users/users.repository.js', () => ({
|
||||||
UsersRepository: class {
|
UsersRepository: class {
|
||||||
findByKeycloakId = vi.fn().mockResolvedValue({ householdIds: ['hh1'] });
|
findByKeycloakId = vi.fn().mockResolvedValue({ householdIds: ['hh1'] });
|
||||||
upsertFromToken = vi.fn().mockResolvedValue({ householdIds: ['hh1'] });
|
upsertFromToken = vi.fn().mockResolvedValue({ householdIds: ['hh1'] });
|
||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
|
|
||||||
import authPlugin from '../../plugins/auth.plugin.js';
|
import authPlugin from '../../../src/plugins/auth.plugin.js';
|
||||||
import householdPlugin from '../../plugins/household.plugin.js';
|
import householdPlugin from '../../../src/plugins/household.plugin.js';
|
||||||
import usersRoutes from '../users/users.routes.js';
|
import usersRoutes from '../../../src/modules/users/users.routes.js';
|
||||||
import organizerRoutes from './organizer.routes.js';
|
import organizerRoutes from '../../../src/modules/organizer/organizer.routes.js';
|
||||||
|
|
||||||
function makeFakeFill(overrides = {}) {
|
function makeFakeFill(overrides = {}) {
|
||||||
return {
|
return {
|
||||||
|
|
@ -19,7 +19,7 @@ vi.mock('mongoose', () => {
|
||||||
return { default: { startSession: vi.fn().mockResolvedValue(mockSession) } };
|
return { default: { startSession: vi.fn().mockResolvedValue(mockSession) } };
|
||||||
});
|
});
|
||||||
|
|
||||||
import { OrganizerService } from './organizer.service.js';
|
import { OrganizerService } from '../../../src/modules/organizer/organizer.service.js';
|
||||||
|
|
||||||
describe(OrganizerService.name, () => {
|
describe(OrganizerService.name, () => {
|
||||||
const mockOrganizerRepo = {
|
const mockOrganizerRepo = {
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import { describe, it, expect } from 'vitest';
|
import { describe, it, expect } from 'vitest';
|
||||||
import { FreshnessCalculatorService } from './freshness-calculator.service.js';
|
import { FreshnessCalculatorService } from '../../../src/modules/pantry/freshness-calculator.service.js';
|
||||||
import { ItemStatus, FreshnessUrgency, FreshnessSource, StorageLocation } from '@meshitrack/shared';
|
import { ItemStatus, FreshnessUrgency, FreshnessSource, StorageLocation } from '@meshitrack/shared';
|
||||||
|
|
||||||
describe(FreshnessCalculatorService.name, () => {
|
describe(FreshnessCalculatorService.name, () => {
|
||||||
|
|
@ -20,7 +20,7 @@ const {
|
||||||
mockFindByIdAndUpdate: vi.fn(),
|
mockFindByIdAndUpdate: vi.fn(),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
vi.mock('../../schemas/pantry-item.schema.js', () => {
|
vi.mock('../../../src/schemas/pantry-item.schema.js', () => {
|
||||||
const chain = () => ({
|
const chain = () => ({
|
||||||
sort: vi.fn().mockReturnThis(),
|
sort: vi.fn().mockReturnThis(),
|
||||||
limit: vi.fn().mockReturnThis(),
|
limit: vi.fn().mockReturnThis(),
|
||||||
|
|
@ -75,7 +75,7 @@ vi.mock('../../schemas/pantry-item.schema.js', () => {
|
||||||
return { PantryItemModel: FakeModel };
|
return { PantryItemModel: FakeModel };
|
||||||
});
|
});
|
||||||
|
|
||||||
import { PantryRepository } from './pantry.repository.js';
|
import { PantryRepository } from '../../../src/modules/pantry/pantry.repository.js';
|
||||||
|
|
||||||
describe(PantryRepository.name, () => {
|
describe(PantryRepository.name, () => {
|
||||||
let repo: PantryRepository;
|
let repo: PantryRepository;
|
||||||
|
|
@ -54,7 +54,7 @@ const { mockFindApplicableRule } = vi.hoisted(() => ({
|
||||||
mockFindApplicableRule: vi.fn(),
|
mockFindApplicableRule: vi.fn(),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
vi.mock('./pantry.repository.js', () => ({
|
vi.mock('../../../src/modules/pantry/pantry.repository.js', () => ({
|
||||||
PantryRepository: class {
|
PantryRepository: class {
|
||||||
findByHousehold = mockFindByHousehold;
|
findByHousehold = mockFindByHousehold;
|
||||||
findById = mockFindById;
|
findById = mockFindById;
|
||||||
|
|
@ -71,14 +71,14 @@ vi.mock('./pantry.repository.js', () => ({
|
||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
|
|
||||||
vi.mock('../products/products.repository.js', () => ({
|
vi.mock('../../../src/modules/products/products.repository.js', () => ({
|
||||||
ProductsRepository: class {
|
ProductsRepository: class {
|
||||||
findById = mockProductFindById;
|
findById = mockProductFindById;
|
||||||
findByIds = vi.fn();
|
findByIds = vi.fn();
|
||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
|
|
||||||
vi.mock('../freshness-rules/freshness-rules.repository.js', () => ({
|
vi.mock('../../../src/modules/freshness-rules/freshness-rules.repository.js', () => ({
|
||||||
FreshnessRulesRepository: class {
|
FreshnessRulesRepository: class {
|
||||||
findApplicableRule = mockFindApplicableRule;
|
findApplicableRule = mockFindApplicableRule;
|
||||||
findByHousehold = vi.fn();
|
findByHousehold = vi.fn();
|
||||||
|
|
@ -89,17 +89,17 @@ vi.mock('../freshness-rules/freshness-rules.repository.js', () => ({
|
||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
|
|
||||||
vi.mock('../users/users.repository.js', () => ({
|
vi.mock('../../../src/modules/users/users.repository.js', () => ({
|
||||||
UsersRepository: class {
|
UsersRepository: class {
|
||||||
findByKeycloakId = vi.fn().mockResolvedValue({ householdIds: ['hh1'] });
|
findByKeycloakId = vi.fn().mockResolvedValue({ householdIds: ['hh1'] });
|
||||||
upsertFromToken = vi.fn().mockResolvedValue({ householdIds: ['hh1'] });
|
upsertFromToken = vi.fn().mockResolvedValue({ householdIds: ['hh1'] });
|
||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
|
|
||||||
import authPlugin from '../../plugins/auth.plugin.js';
|
import authPlugin from '../../../src/plugins/auth.plugin.js';
|
||||||
import householdPlugin from '../../plugins/household.plugin.js';
|
import householdPlugin from '../../../src/plugins/household.plugin.js';
|
||||||
import usersRoutes from '../users/users.routes.js';
|
import usersRoutes from '../../../src/modules/users/users.routes.js';
|
||||||
import pantryRoutes from './pantry.routes.js';
|
import pantryRoutes from '../../../src/modules/pantry/pantry.routes.js';
|
||||||
|
|
||||||
const freshness = {
|
const freshness = {
|
||||||
estimatedExpiryDate: new Date('2024-02-01').toISOString(),
|
estimatedExpiryDate: new Date('2024-02-01').toISOString(),
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||||
import { PantryService } from './pantry.service.js';
|
import { PantryService } from '../../../src/modules/pantry/pantry.service.js';
|
||||||
import { NotFoundError, BadRequestError } from '../../common/errors.js';
|
import { NotFoundError, BadRequestError } from '../../../src/common/errors.js';
|
||||||
import { ItemStatus } from '@meshitrack/shared';
|
import { ItemStatus } from '@meshitrack/shared';
|
||||||
|
|
||||||
const mockPantryRepo = {
|
const mockPantryRepo = {
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||||
import { PricesRepository } from './prices.repository.js';
|
import { PricesRepository } from '../../../src/modules/prices/prices.repository.js';
|
||||||
|
|
||||||
const { mockSave, MockPriceRecordModel } = vi.hoisted(() => {
|
const { mockSave, MockPriceRecordModel } = vi.hoisted(() => {
|
||||||
const mockSave = vi.fn();
|
const mockSave = vi.fn();
|
||||||
|
|
@ -17,11 +17,11 @@ const { mockSave, MockPriceRecordModel } = vi.hoisted(() => {
|
||||||
return { mockSave, MockPriceRecordModel: MockModel };
|
return { mockSave, MockPriceRecordModel: MockModel };
|
||||||
});
|
});
|
||||||
|
|
||||||
vi.mock('../../schemas/price-record.schema.js', () => ({
|
vi.mock('../../../src/schemas/price-record.schema.js', () => ({
|
||||||
PriceRecordModel: MockPriceRecordModel,
|
PriceRecordModel: MockPriceRecordModel,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const { PriceRecordModel } = await import('../../schemas/price-record.schema.js');
|
const { PriceRecordModel } = await import('../../../src/schemas/price-record.schema.js');
|
||||||
|
|
||||||
function makeChain(result: unknown = null) {
|
function makeChain(result: unknown = null) {
|
||||||
return {
|
return {
|
||||||
|
|
@ -23,7 +23,7 @@ const mockFindByProduct = vi.fn();
|
||||||
const mockCompareStores = vi.fn();
|
const mockCompareStores = vi.fn();
|
||||||
const mockGetAnalytics = vi.fn();
|
const mockGetAnalytics = vi.fn();
|
||||||
|
|
||||||
vi.mock('./prices.repository.js', () => ({
|
vi.mock('../../../src/modules/prices/prices.repository.js', () => ({
|
||||||
PricesRepository: class {
|
PricesRepository: class {
|
||||||
create = mockCreate;
|
create = mockCreate;
|
||||||
createMany = mockCreateMany;
|
createMany = mockCreateMany;
|
||||||
|
|
@ -33,30 +33,30 @@ vi.mock('./prices.repository.js', () => ({
|
||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
|
|
||||||
vi.mock('../products/products.repository.js', () => ({
|
vi.mock('../../../src/modules/products/products.repository.js', () => ({
|
||||||
ProductsRepository: class {
|
ProductsRepository: class {
|
||||||
findById = vi.fn().mockResolvedValue({ name: 'Mock Product' });
|
findById = vi.fn().mockResolvedValue({ name: 'Mock Product' });
|
||||||
findByIds = vi.fn().mockResolvedValue([{ _id: 'p1', name: 'Mock Product' }]);
|
findByIds = vi.fn().mockResolvedValue([{ _id: 'p1', name: 'Mock Product' }]);
|
||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
|
|
||||||
vi.mock('../stores/stores.repository.js', () => ({
|
vi.mock('../../../src/modules/stores/stores.repository.js', () => ({
|
||||||
StoresRepository: class {
|
StoresRepository: class {
|
||||||
findById = vi.fn().mockResolvedValue({ name: 'Mock Store' });
|
findById = vi.fn().mockResolvedValue({ name: 'Mock Store' });
|
||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
|
|
||||||
vi.mock('../users/users.repository.js', () => ({
|
vi.mock('../../../src/modules/users/users.repository.js', () => ({
|
||||||
UsersRepository: class {
|
UsersRepository: class {
|
||||||
findByKeycloakId = vi.fn().mockResolvedValue({ householdIds: ['hh1'] });
|
findByKeycloakId = vi.fn().mockResolvedValue({ householdIds: ['hh1'] });
|
||||||
upsertFromToken = vi.fn().mockResolvedValue({ householdIds: ['hh1'] });
|
upsertFromToken = vi.fn().mockResolvedValue({ householdIds: ['hh1'] });
|
||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
|
|
||||||
import authPlugin from '../../plugins/auth.plugin.js';
|
import authPlugin from '../../../src/plugins/auth.plugin.js';
|
||||||
import householdPlugin from '../../plugins/household.plugin.js';
|
import householdPlugin from '../../../src/plugins/household.plugin.js';
|
||||||
import usersRoutes from '../users/users.routes.js';
|
import usersRoutes from '../../../src/modules/users/users.routes.js';
|
||||||
import pricesRoutes from './prices.routes.js';
|
import pricesRoutes from '../../../src/modules/prices/prices.routes.js';
|
||||||
|
|
||||||
describe('prices.routes', () => {
|
describe('prices.routes', () => {
|
||||||
let app: any;
|
let app: any;
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||||
import { PricesService } from './prices.service.js';
|
import { PricesService } from '../../../src/modules/prices/prices.service.js';
|
||||||
import { NotFoundError } from '../../common/errors.js';
|
import { NotFoundError } from '../../../src/common/errors.js';
|
||||||
|
|
||||||
describe('PricesService', () => {
|
describe('PricesService', () => {
|
||||||
let service: PricesService;
|
let service: PricesService;
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||||
import { BarcodeService } from './barcode.service.js';
|
import { BarcodeService } from '../../../src/modules/products/barcode.service.js';
|
||||||
|
|
||||||
vi.mock('undici', () => ({
|
vi.mock('undici', () => ({
|
||||||
request: vi.fn(),
|
request: vi.fn(),
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import { describe, it, expect } from 'vitest';
|
import { describe, it, expect } from 'vitest';
|
||||||
import { parseCsv, MAX_FILE_SIZE, MAX_ROWS } from './csv-parser.js';
|
import { parseCsv, MAX_FILE_SIZE, MAX_ROWS } from '../../../src/modules/products/csv-parser.js';
|
||||||
import { ProductCategory, ServingUnit, ProductSource } from '@meshitrack/shared';
|
import { ProductCategory, ServingUnit, ProductSource } from '@meshitrack/shared';
|
||||||
|
|
||||||
describe('parseCsv', () => {
|
describe('parseCsv', () => {
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||||
import { ProductsRepository } from './products.repository.js';
|
import { ProductsRepository } from '../../../src/modules/products/products.repository.js';
|
||||||
|
|
||||||
const { mockSave, MockProductModel } = vi.hoisted(() => {
|
const { mockSave, MockProductModel } = vi.hoisted(() => {
|
||||||
const mockSave = vi.fn();
|
const mockSave = vi.fn();
|
||||||
|
|
@ -16,11 +16,11 @@ const { mockSave, MockProductModel } = vi.hoisted(() => {
|
||||||
return { mockSave, MockProductModel };
|
return { mockSave, MockProductModel };
|
||||||
});
|
});
|
||||||
|
|
||||||
vi.mock('../../schemas/product.schema.js', () => ({
|
vi.mock('../../../src/schemas/product.schema.js', () => ({
|
||||||
ProductModel: MockProductModel,
|
ProductModel: MockProductModel,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const { ProductModel } = await import('../../schemas/product.schema.js');
|
const { ProductModel } = await import('../../../src/schemas/product.schema.js');
|
||||||
|
|
||||||
function makeChain(result: unknown = null) {
|
function makeChain(result: unknown = null) {
|
||||||
return {
|
return {
|
||||||
|
|
@ -41,7 +41,7 @@ const {
|
||||||
mockBarcodeLookup: vi.fn(),
|
mockBarcodeLookup: vi.fn(),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
vi.mock('./products.repository.js', () => ({
|
vi.mock('../../../src/modules/products/products.repository.js', () => ({
|
||||||
ProductsRepository: class {
|
ProductsRepository: class {
|
||||||
findByHousehold = mockFindByHousehold;
|
findByHousehold = mockFindByHousehold;
|
||||||
findById = mockFindById;
|
findById = mockFindById;
|
||||||
|
|
@ -55,23 +55,23 @@ vi.mock('./products.repository.js', () => ({
|
||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
|
|
||||||
vi.mock('./barcode.service.js', () => ({
|
vi.mock('../../../src/modules/products/barcode.service.js', () => ({
|
||||||
BarcodeService: class {
|
BarcodeService: class {
|
||||||
lookup = mockBarcodeLookup;
|
lookup = mockBarcodeLookup;
|
||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
|
|
||||||
vi.mock('../users/users.repository.js', () => ({
|
vi.mock('../../../src/modules/users/users.repository.js', () => ({
|
||||||
UsersRepository: class {
|
UsersRepository: class {
|
||||||
findByKeycloakId = vi.fn().mockResolvedValue({ householdIds: ['hh1'] });
|
findByKeycloakId = vi.fn().mockResolvedValue({ householdIds: ['hh1'] });
|
||||||
upsertFromToken = vi.fn().mockResolvedValue({ householdIds: ['hh1'] });
|
upsertFromToken = vi.fn().mockResolvedValue({ householdIds: ['hh1'] });
|
||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
|
|
||||||
import authPlugin from '../../plugins/auth.plugin.js';
|
import authPlugin from '../../../src/plugins/auth.plugin.js';
|
||||||
import householdPlugin from '../../plugins/household.plugin.js';
|
import householdPlugin from '../../../src/plugins/household.plugin.js';
|
||||||
import usersRoutes from '../users/users.routes.js';
|
import usersRoutes from '../../../src/modules/users/users.routes.js';
|
||||||
import productsRoutes from './products.routes.js';
|
import productsRoutes from '../../../src/modules/products/products.routes.js';
|
||||||
|
|
||||||
function makeFakeProduct(overrides: Record<string, unknown> = {}) {
|
function makeFakeProduct(overrides: Record<string, unknown> = {}) {
|
||||||
return {
|
return {
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||||
import { ProductsService } from './products.service.js';
|
import { ProductsService } from '../../../src/modules/products/products.service.js';
|
||||||
import { NotFoundError, ConflictError } from '../../common/errors.js';
|
import { NotFoundError, ConflictError } from '../../../src/common/errors.js';
|
||||||
import { ProductCategory, ServingUnit, ProductSource } from '@meshitrack/shared';
|
import { ProductCategory, ServingUnit, ProductSource } from '@meshitrack/shared';
|
||||||
|
|
||||||
const mockRepo = {
|
const mockRepo = {
|
||||||
|
|
@ -8,7 +8,7 @@ const { mockFind, mockFindOne, mockFindOneAndUpdate, mockAggregate, mockSave } =
|
||||||
mockSave: vi.fn(),
|
mockSave: vi.fn(),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
vi.mock('../../schemas/purchase.schema.js', () => {
|
vi.mock('../../../src/schemas/purchase.schema.js', () => {
|
||||||
const findChain = () => ({
|
const findChain = () => ({
|
||||||
sort: vi.fn().mockReturnThis(),
|
sort: vi.fn().mockReturnThis(),
|
||||||
limit: vi.fn().mockReturnThis(),
|
limit: vi.fn().mockReturnThis(),
|
||||||
|
|
@ -36,7 +36,7 @@ vi.mock('../../schemas/purchase.schema.js', () => {
|
||||||
return { PurchaseModel: FakeModel };
|
return { PurchaseModel: FakeModel };
|
||||||
});
|
});
|
||||||
|
|
||||||
import { PurchasesRepository } from './purchases.repository.js';
|
import { PurchasesRepository } from '../../../src/modules/purchases/purchases.repository.js';
|
||||||
|
|
||||||
const makeItem = (overrides = {}) => ({
|
const makeItem = (overrides = {}) => ({
|
||||||
medicineProductId: 'mp-1',
|
medicineProductId: 'mp-1',
|
||||||
|
|
@ -101,7 +101,7 @@ describe(PurchasesRepository.name, () => {
|
||||||
|
|
||||||
it('filters by status when provided', async () => {
|
it('filters by status when provided', async () => {
|
||||||
mockFind.mockResolvedValue([]);
|
mockFind.mockResolvedValue([]);
|
||||||
const { PurchaseModel } = await import('../../schemas/purchase.schema.js');
|
const { PurchaseModel } = await import('../../../src/schemas/purchase.schema.js');
|
||||||
|
|
||||||
await repo.findByHousehold('hh1', { limit: 20, status: 'ordered' });
|
await repo.findByHousehold('hh1', { limit: 20, status: 'ordered' });
|
||||||
|
|
||||||
|
|
@ -112,7 +112,7 @@ describe(PurchasesRepository.name, () => {
|
||||||
|
|
||||||
it('filters by storeId when provided', async () => {
|
it('filters by storeId when provided', async () => {
|
||||||
mockFind.mockResolvedValue([]);
|
mockFind.mockResolvedValue([]);
|
||||||
const { PurchaseModel } = await import('../../schemas/purchase.schema.js');
|
const { PurchaseModel } = await import('../../../src/schemas/purchase.schema.js');
|
||||||
|
|
||||||
await repo.findByHousehold('hh1', { limit: 20, storeId: 'st-1' });
|
await repo.findByHousehold('hh1', { limit: 20, storeId: 'st-1' });
|
||||||
|
|
||||||
|
|
@ -122,7 +122,7 @@ describe(PurchasesRepository.name, () => {
|
||||||
it('applies cursor filter when provided', async () => {
|
it('applies cursor filter when provided', async () => {
|
||||||
mockFind.mockResolvedValue([]);
|
mockFind.mockResolvedValue([]);
|
||||||
const cursor = Buffer.from('p-1').toString('base64');
|
const cursor = Buffer.from('p-1').toString('base64');
|
||||||
const { PurchaseModel } = await import('../../schemas/purchase.schema.js');
|
const { PurchaseModel } = await import('../../../src/schemas/purchase.schema.js');
|
||||||
|
|
||||||
await repo.findByHousehold('hh1', { limit: 20, cursor });
|
await repo.findByHousehold('hh1', { limit: 20, cursor });
|
||||||
|
|
||||||
|
|
@ -172,7 +172,7 @@ describe(PurchasesRepository.name, () => {
|
||||||
it('includes items in update set when provided', async () => {
|
it('includes items in update set when provided', async () => {
|
||||||
const updated = { _id: 'p-1' };
|
const updated = { _id: 'p-1' };
|
||||||
mockFindOneAndUpdate.mockResolvedValue(updated);
|
mockFindOneAndUpdate.mockResolvedValue(updated);
|
||||||
const { PurchaseModel } = await import('../../schemas/purchase.schema.js');
|
const { PurchaseModel } = await import('../../../src/schemas/purchase.schema.js');
|
||||||
const items = [{ name: 'X', quantity: 1, unit: 'tablet', addedToCabinet: false }];
|
const items = [{ name: 'X', quantity: 1, unit: 'tablet', addedToCabinet: false }];
|
||||||
|
|
||||||
await repo.update('p-1', 'hh1', { items } as never);
|
await repo.update('p-1', 'hh1', { items } as never);
|
||||||
|
|
@ -189,7 +189,7 @@ describe(PurchasesRepository.name, () => {
|
||||||
it('sets status to in_cabinet and all items addedToCabinet', async () => {
|
it('sets status to in_cabinet and all items addedToCabinet', async () => {
|
||||||
const updated = { _id: 'p-1', status: 'in_cabinet' };
|
const updated = { _id: 'p-1', status: 'in_cabinet' };
|
||||||
mockFindOneAndUpdate.mockResolvedValue(updated);
|
mockFindOneAndUpdate.mockResolvedValue(updated);
|
||||||
const { PurchaseModel } = await import('../../schemas/purchase.schema.js');
|
const { PurchaseModel } = await import('../../../src/schemas/purchase.schema.js');
|
||||||
|
|
||||||
const result = await repo.receiveAll('p-1', 'hh1');
|
const result = await repo.receiveAll('p-1', 'hh1');
|
||||||
|
|
||||||
|
|
@ -208,7 +208,7 @@ describe(PurchasesRepository.name, () => {
|
||||||
it('builds per-index update set and calls findOneAndUpdate', async () => {
|
it('builds per-index update set and calls findOneAndUpdate', async () => {
|
||||||
const updated = { _id: 'p-1', items: [] };
|
const updated = { _id: 'p-1', items: [] };
|
||||||
mockFindOneAndUpdate.mockResolvedValue(updated);
|
mockFindOneAndUpdate.mockResolvedValue(updated);
|
||||||
const { PurchaseModel } = await import('../../schemas/purchase.schema.js');
|
const { PurchaseModel } = await import('../../../src/schemas/purchase.schema.js');
|
||||||
|
|
||||||
const result = await repo.markItemsAddedToCabinet('p-1', 'hh1', [0, 2]);
|
const result = await repo.markItemsAddedToCabinet('p-1', 'hh1', [0, 2]);
|
||||||
|
|
||||||
|
|
@ -29,7 +29,7 @@ const { mockList, mockGetById, mockCreate, mockUpdate, mockReceive, mockDelete }
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
vi.mock('./purchases.repository.js', () => ({
|
vi.mock('../../../src/modules/purchases/purchases.repository.js', () => ({
|
||||||
PurchasesRepository: class {
|
PurchasesRepository: class {
|
||||||
create = vi.fn();
|
create = vi.fn();
|
||||||
findByHousehold = vi.fn();
|
findByHousehold = vi.fn();
|
||||||
|
|
@ -41,7 +41,7 @@ vi.mock('./purchases.repository.js', () => ({
|
||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
|
|
||||||
vi.mock('./purchases.service.js', () => ({
|
vi.mock('../../../src/modules/purchases/purchases.service.js', () => ({
|
||||||
PurchasesService: class {
|
PurchasesService: class {
|
||||||
list = mockList;
|
list = mockList;
|
||||||
getById = mockGetById;
|
getById = mockGetById;
|
||||||
|
|
@ -52,17 +52,17 @@ vi.mock('./purchases.service.js', () => ({
|
||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
|
|
||||||
vi.mock('../users/users.repository.js', () => ({
|
vi.mock('../../../src/modules/users/users.repository.js', () => ({
|
||||||
UsersRepository: class {
|
UsersRepository: class {
|
||||||
findByKeycloakId = vi.fn().mockResolvedValue({ householdIds: ['hh1'] });
|
findByKeycloakId = vi.fn().mockResolvedValue({ householdIds: ['hh1'] });
|
||||||
upsertFromToken = vi.fn().mockResolvedValue({ householdIds: ['hh1'] });
|
upsertFromToken = vi.fn().mockResolvedValue({ householdIds: ['hh1'] });
|
||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
|
|
||||||
import authPlugin from '../../plugins/auth.plugin.js';
|
import authPlugin from '../../../src/plugins/auth.plugin.js';
|
||||||
import householdPlugin from '../../plugins/household.plugin.js';
|
import householdPlugin from '../../../src/plugins/household.plugin.js';
|
||||||
import usersRoutes from '../users/users.routes.js';
|
import usersRoutes from '../../../src/modules/users/users.routes.js';
|
||||||
import purchasesRoutes from './purchases.routes.js';
|
import purchasesRoutes from '../../../src/modules/purchases/purchases.routes.js';
|
||||||
|
|
||||||
function makeFakePurchase(overrides = {}) {
|
function makeFakePurchase(overrides = {}) {
|
||||||
return {
|
return {
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||||
import { PurchasesService } from './purchases.service.js';
|
import { PurchasesService } from '../../../src/modules/purchases/purchases.service.js';
|
||||||
|
|
||||||
describe(PurchasesService.name, () => {
|
describe(PurchasesService.name, () => {
|
||||||
const mockPurchasesRepo = {
|
const mockPurchasesRepo = {
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import { describe, it, expect } from 'vitest';
|
import { describe, it, expect } from 'vitest';
|
||||||
import { NutritionCalculatorService } from './nutrition-calculator.service.js';
|
import { NutritionCalculatorService } from '../../../src/modules/recipes/nutrition-calculator.service.js';
|
||||||
import { NutritionWarning } from '@meshitrack/shared';
|
import { NutritionWarning } from '@meshitrack/shared';
|
||||||
|
|
||||||
const service = new NutritionCalculatorService();
|
const service = new NutritionCalculatorService();
|
||||||
|
|
@ -7,7 +7,7 @@ const { mockFind, mockFindOne, mockFindOneAndUpdate, mockSave } = vi.hoisted(()
|
||||||
mockSave: vi.fn(),
|
mockSave: vi.fn(),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
vi.mock('../../schemas/recipe.schema.js', () => {
|
vi.mock('../../../src/schemas/recipe.schema.js', () => {
|
||||||
const chain = () => ({
|
const chain = () => ({
|
||||||
sort: vi.fn().mockReturnThis(),
|
sort: vi.fn().mockReturnThis(),
|
||||||
limit: vi.fn().mockReturnThis(),
|
limit: vi.fn().mockReturnThis(),
|
||||||
|
|
@ -41,7 +41,7 @@ vi.mock('../../schemas/recipe.schema.js', () => {
|
||||||
return { RecipeModel: FakeModel };
|
return { RecipeModel: FakeModel };
|
||||||
});
|
});
|
||||||
|
|
||||||
import { RecipesRepository } from './recipes.repository.js';
|
import { RecipesRepository } from '../../../src/modules/recipes/recipes.repository.js';
|
||||||
|
|
||||||
describe(RecipesRepository.name, () => {
|
describe(RecipesRepository.name, () => {
|
||||||
let repo: RecipesRepository;
|
let repo: RecipesRepository;
|
||||||
|
|
@ -40,7 +40,7 @@ const { mockFindByIds } = vi.hoisted(() => ({
|
||||||
mockFindByIds: vi.fn(),
|
mockFindByIds: vi.fn(),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
vi.mock('./recipes.repository.js', () => ({
|
vi.mock('../../../src/modules/recipes/recipes.repository.js', () => ({
|
||||||
RecipesRepository: class {
|
RecipesRepository: class {
|
||||||
findByHousehold = mockFindByHousehold;
|
findByHousehold = mockFindByHousehold;
|
||||||
findById = mockFindById;
|
findById = mockFindById;
|
||||||
|
|
@ -52,24 +52,24 @@ vi.mock('./recipes.repository.js', () => ({
|
||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
|
|
||||||
vi.mock('../products/products.repository.js', () => ({
|
vi.mock('../../../src/modules/products/products.repository.js', () => ({
|
||||||
ProductsRepository: class {
|
ProductsRepository: class {
|
||||||
findByIds = mockFindByIds;
|
findByIds = mockFindByIds;
|
||||||
findById = vi.fn();
|
findById = vi.fn();
|
||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
|
|
||||||
vi.mock('../users/users.repository.js', () => ({
|
vi.mock('../../../src/modules/users/users.repository.js', () => ({
|
||||||
UsersRepository: class {
|
UsersRepository: class {
|
||||||
findByKeycloakId = vi.fn().mockResolvedValue({ householdIds: ['hh1'] });
|
findByKeycloakId = vi.fn().mockResolvedValue({ householdIds: ['hh1'] });
|
||||||
upsertFromToken = vi.fn().mockResolvedValue({ householdIds: ['hh1'] });
|
upsertFromToken = vi.fn().mockResolvedValue({ householdIds: ['hh1'] });
|
||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
|
|
||||||
import authPlugin from '../../plugins/auth.plugin.js';
|
import authPlugin from '../../../src/plugins/auth.plugin.js';
|
||||||
import householdPlugin from '../../plugins/household.plugin.js';
|
import householdPlugin from '../../../src/plugins/household.plugin.js';
|
||||||
import usersRoutes from '../users/users.routes.js';
|
import usersRoutes from '../../../src/modules/users/users.routes.js';
|
||||||
import recipesRoutes from './recipes.routes.js';
|
import recipesRoutes from '../../../src/modules/recipes/recipes.routes.js';
|
||||||
|
|
||||||
const nutrition = { calories: 200, protein: 20, carbs: 10, fat: 8 };
|
const nutrition = { calories: 200, protein: 20, carbs: 10, fat: 8 };
|
||||||
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||||
import { RecipesService } from './recipes.service.js';
|
import { RecipesService } from '../../../src/modules/recipes/recipes.service.js';
|
||||||
import { NotFoundError, BadRequestError } from '../../common/errors.js';
|
import { NotFoundError, BadRequestError } from '../../../src/common/errors.js';
|
||||||
|
|
||||||
const makeProduct = (id: string, servingUnit = 'g', servingSize = 100) => ({
|
const makeProduct = (id: string, servingUnit = 'g', servingSize = 100) => ({
|
||||||
_id: { toString: () => id },
|
_id: { toString: () => id },
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import { describe, it, expect } from 'vitest';
|
import { describe, it, expect } from 'vitest';
|
||||||
import { toMetric } from './unit-conversion.service.js';
|
import { toMetric } from '../../../src/modules/recipes/unit-conversion.service.js';
|
||||||
|
|
||||||
describe('toMetric', () => {
|
describe('toMetric', () => {
|
||||||
describe('metric pass-through', () => {
|
describe('metric pass-through', () => {
|
||||||
|
|
@ -7,7 +7,7 @@ const { mockFind, mockFindOne, mockFindOneAndUpdate, mockSave } = vi.hoisted(()
|
||||||
mockSave: vi.fn(),
|
mockSave: vi.fn(),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
vi.mock('../../schemas/refill-list.schema.js', () => {
|
vi.mock('../../../src/schemas/refill-list.schema.js', () => {
|
||||||
const chain = () => ({
|
const chain = () => ({
|
||||||
sort: vi.fn().mockReturnThis(),
|
sort: vi.fn().mockReturnThis(),
|
||||||
limit: vi.fn().mockReturnThis(),
|
limit: vi.fn().mockReturnThis(),
|
||||||
|
|
@ -33,7 +33,7 @@ vi.mock('../../schemas/refill-list.schema.js', () => {
|
||||||
return { RefillListModel: FakeModel };
|
return { RefillListModel: FakeModel };
|
||||||
});
|
});
|
||||||
|
|
||||||
import { RefillsRepository } from './refills.repository.js';
|
import { RefillsRepository } from '../../../src/modules/refills/refills.repository.js';
|
||||||
|
|
||||||
describe(RefillsRepository.name, () => {
|
describe(RefillsRepository.name, () => {
|
||||||
let repo: RefillsRepository;
|
let repo: RefillsRepository;
|
||||||
|
|
@ -38,7 +38,7 @@ const {
|
||||||
mockGetStoreComparison: vi.fn(),
|
mockGetStoreComparison: vi.fn(),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
vi.mock('./refills.repository.js', () => ({
|
vi.mock('../../../src/modules/refills/refills.repository.js', () => ({
|
||||||
RefillsRepository: class {
|
RefillsRepository: class {
|
||||||
create = vi.fn();
|
create = vi.fn();
|
||||||
findByHousehold = vi.fn();
|
findByHousehold = vi.fn();
|
||||||
|
|
@ -49,7 +49,7 @@ vi.mock('./refills.repository.js', () => ({
|
||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
|
|
||||||
vi.mock('./refills.service.js', () => ({
|
vi.mock('../../../src/modules/refills/refills.service.js', () => ({
|
||||||
RefillsService: class {
|
RefillsService: class {
|
||||||
getAlerts = mockGetAlerts;
|
getAlerts = mockGetAlerts;
|
||||||
createList = mockCreateList;
|
createList = mockCreateList;
|
||||||
|
|
@ -62,17 +62,17 @@ vi.mock('./refills.service.js', () => ({
|
||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
|
|
||||||
vi.mock('../users/users.repository.js', () => ({
|
vi.mock('../../../src/modules/users/users.repository.js', () => ({
|
||||||
UsersRepository: class {
|
UsersRepository: class {
|
||||||
findByKeycloakId = vi.fn().mockResolvedValue({ householdIds: ['hh1'] });
|
findByKeycloakId = vi.fn().mockResolvedValue({ householdIds: ['hh1'] });
|
||||||
upsertFromToken = vi.fn().mockResolvedValue({ householdIds: ['hh1'] });
|
upsertFromToken = vi.fn().mockResolvedValue({ householdIds: ['hh1'] });
|
||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
|
|
||||||
import authPlugin from '../../plugins/auth.plugin.js';
|
import authPlugin from '../../../src/plugins/auth.plugin.js';
|
||||||
import householdPlugin from '../../plugins/household.plugin.js';
|
import householdPlugin from '../../../src/plugins/household.plugin.js';
|
||||||
import usersRoutes from '../users/users.routes.js';
|
import usersRoutes from '../../../src/modules/users/users.routes.js';
|
||||||
import refillsRoutes from './refills.routes.js';
|
import refillsRoutes from '../../../src/modules/refills/refills.routes.js';
|
||||||
|
|
||||||
function makeFakeRefillList(overrides = {}) {
|
function makeFakeRefillList(overrides = {}) {
|
||||||
return {
|
return {
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||||
import { RefillsService } from './refills.service.js';
|
import { RefillsService } from '../../../src/modules/refills/refills.service.js';
|
||||||
|
|
||||||
describe(RefillsService.name, () => {
|
describe(RefillsService.name, () => {
|
||||||
const mockRepo = {
|
const mockRepo = {
|
||||||
|
|
@ -7,7 +7,7 @@ const { mockFind, mockFindOne, mockFindOneAndUpdate, mockSave } = vi.hoisted(()
|
||||||
mockSave: vi.fn(),
|
mockSave: vi.fn(),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
vi.mock('../../schemas/regimen.schema.js', () => {
|
vi.mock('../../../src/schemas/regimen.schema.js', () => {
|
||||||
const chain = () => ({
|
const chain = () => ({
|
||||||
sort: vi.fn().mockReturnThis(),
|
sort: vi.fn().mockReturnThis(),
|
||||||
limit: vi.fn().mockReturnThis(),
|
limit: vi.fn().mockReturnThis(),
|
||||||
|
|
@ -41,7 +41,7 @@ vi.mock('../../schemas/regimen.schema.js', () => {
|
||||||
return { RegimenModel: FakeModel };
|
return { RegimenModel: FakeModel };
|
||||||
});
|
});
|
||||||
|
|
||||||
import { RegimensRepository } from './regimens.repository.js';
|
import { RegimensRepository } from '../../../src/modules/regimens/regimens.repository.js';
|
||||||
|
|
||||||
describe(RegimensRepository.name, () => {
|
describe(RegimensRepository.name, () => {
|
||||||
let repo: RegimensRepository;
|
let repo: RegimensRepository;
|
||||||
|
|
@ -29,7 +29,7 @@ const { mockList, mockGetById, mockCreate, mockUpdate, mockDelete, mockCalculate
|
||||||
mockCalculateBurnRates: vi.fn(),
|
mockCalculateBurnRates: vi.fn(),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
vi.mock('./regimens.repository.js', () => ({
|
vi.mock('../../../src/modules/regimens/regimens.repository.js', () => ({
|
||||||
RegimensRepository: class {
|
RegimensRepository: class {
|
||||||
findByHousehold = vi.fn();
|
findByHousehold = vi.fn();
|
||||||
findById = vi.fn();
|
findById = vi.fn();
|
||||||
|
|
@ -41,7 +41,7 @@ vi.mock('./regimens.repository.js', () => ({
|
||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
|
|
||||||
vi.mock('./regimens.service.js', () => ({
|
vi.mock('../../../src/modules/regimens/regimens.service.js', () => ({
|
||||||
RegimensService: class {
|
RegimensService: class {
|
||||||
list = mockList;
|
list = mockList;
|
||||||
getById = mockGetById;
|
getById = mockGetById;
|
||||||
|
|
@ -53,17 +53,17 @@ vi.mock('./regimens.service.js', () => ({
|
||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
|
|
||||||
vi.mock('../users/users.repository.js', () => ({
|
vi.mock('../../../src/modules/users/users.repository.js', () => ({
|
||||||
UsersRepository: class {
|
UsersRepository: class {
|
||||||
findByKeycloakId = vi.fn().mockResolvedValue({ householdIds: ['hh1'] });
|
findByKeycloakId = vi.fn().mockResolvedValue({ householdIds: ['hh1'] });
|
||||||
upsertFromToken = vi.fn().mockResolvedValue({ householdIds: ['hh1'] });
|
upsertFromToken = vi.fn().mockResolvedValue({ householdIds: ['hh1'] });
|
||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
|
|
||||||
import authPlugin from '../../plugins/auth.plugin.js';
|
import authPlugin from '../../../src/plugins/auth.plugin.js';
|
||||||
import householdPlugin from '../../plugins/household.plugin.js';
|
import householdPlugin from '../../../src/plugins/household.plugin.js';
|
||||||
import usersRoutes from '../users/users.routes.js';
|
import usersRoutes from '../../../src/modules/users/users.routes.js';
|
||||||
import regimensRoutes from './regimens.routes.js';
|
import regimensRoutes from '../../../src/modules/regimens/regimens.routes.js';
|
||||||
|
|
||||||
function makeFakeRegimen(overrides = {}) {
|
function makeFakeRegimen(overrides = {}) {
|
||||||
return {
|
return {
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||||
import { RegimensService } from './regimens.service.js';
|
import { RegimensService } from '../../../src/modules/regimens/regimens.service.js';
|
||||||
import { DosageFrequency } from '@meshitrack/shared';
|
import { DosageFrequency } from '@meshitrack/shared';
|
||||||
|
|
||||||
describe(RegimensService.name, () => {
|
describe(RegimensService.name, () => {
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||||
import { ShoppingListsRepository } from './shopping-lists.repository.js';
|
import { ShoppingListsRepository } from '../../../src/modules/shopping-lists/shopping-lists.repository.js';
|
||||||
|
|
||||||
const { mockSave, MockShoppingListModel } = vi.hoisted(() => {
|
const { mockSave, MockShoppingListModel } = vi.hoisted(() => {
|
||||||
const mockSave = vi.fn();
|
const mockSave = vi.fn();
|
||||||
|
|
@ -16,11 +16,11 @@ const { mockSave, MockShoppingListModel } = vi.hoisted(() => {
|
||||||
return { mockSave, MockShoppingListModel: MockModel };
|
return { mockSave, MockShoppingListModel: MockModel };
|
||||||
});
|
});
|
||||||
|
|
||||||
vi.mock('../../schemas/shopping-list.schema.js', () => ({
|
vi.mock('../../../src/schemas/shopping-list.schema.js', () => ({
|
||||||
ShoppingListModel: MockShoppingListModel,
|
ShoppingListModel: MockShoppingListModel,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const { ShoppingListModel } = await import('../../schemas/shopping-list.schema.js');
|
const { ShoppingListModel } = await import('../../../src/schemas/shopping-list.schema.js');
|
||||||
|
|
||||||
function makeChain(result: unknown = null) {
|
function makeChain(result: unknown = null) {
|
||||||
return {
|
return {
|
||||||
|
|
@ -26,7 +26,7 @@ const mockAddItem = vi.fn();
|
||||||
const mockUpdateItem = vi.fn();
|
const mockUpdateItem = vi.fn();
|
||||||
const mockRemoveItem = vi.fn();
|
const mockRemoveItem = vi.fn();
|
||||||
|
|
||||||
vi.mock('./shopping-lists.repository.js', () => ({
|
vi.mock('../../../src/modules/shopping-lists/shopping-lists.repository.js', () => ({
|
||||||
ShoppingListsRepository: class {
|
ShoppingListsRepository: class {
|
||||||
list = mockList;
|
list = mockList;
|
||||||
findById = mockFindById;
|
findById = mockFindById;
|
||||||
|
|
@ -39,25 +39,25 @@ vi.mock('./shopping-lists.repository.js', () => ({
|
||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
|
|
||||||
vi.mock('../meal-plans/shopping-gap.service.js', () => ({
|
vi.mock('../../../src/modules/meal-plans/shopping-gap.service.js', () => ({
|
||||||
ShoppingGapService: class {
|
ShoppingGapService: class {
|
||||||
calculateGap = vi.fn().mockResolvedValue({ missingItems: [] });
|
calculateGap = vi.fn().mockResolvedValue({ missingItems: [] });
|
||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
|
|
||||||
vi.mock('../pantry/pantry.service.js', () => ({
|
vi.mock('../../../src/modules/pantry/pantry.service.js', () => ({
|
||||||
PantryService: class {
|
PantryService: class {
|
||||||
create = vi.fn().mockResolvedValue({ _id: 'pant1' });
|
create = vi.fn().mockResolvedValue({ _id: 'pant1' });
|
||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
|
|
||||||
vi.mock('../products/products.repository.js', () => ({
|
vi.mock('../../../src/modules/products/products.repository.js', () => ({
|
||||||
ProductsRepository: class {
|
ProductsRepository: class {
|
||||||
findById = vi.fn().mockResolvedValue({ category: 'dairy' });
|
findById = vi.fn().mockResolvedValue({ category: 'dairy' });
|
||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
|
|
||||||
vi.mock('../prices/prices.service.js', () => ({
|
vi.mock('../../../src/modules/prices/prices.service.js', () => ({
|
||||||
PricesService: class {
|
PricesService: class {
|
||||||
estimatePrice = vi.fn().mockResolvedValue(5.0);
|
estimatePrice = vi.fn().mockResolvedValue(5.0);
|
||||||
recordPrice = vi.fn().mockResolvedValue({});
|
recordPrice = vi.fn().mockResolvedValue({});
|
||||||
|
|
@ -65,24 +65,24 @@ vi.mock('../prices/prices.service.js', () => ({
|
||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
|
|
||||||
vi.mock('../meal-plans/meal-plans.repository.js', () => ({
|
vi.mock('../../../src/modules/meal-plans/meal-plans.repository.js', () => ({
|
||||||
MealPlanRepository: class {
|
MealPlanRepository: class {
|
||||||
findById = vi.fn().mockResolvedValue({ _id: 'mp1', weekStartDate: new Date() });
|
findById = vi.fn().mockResolvedValue({ _id: 'mp1', weekStartDate: new Date() });
|
||||||
update = vi.fn().mockResolvedValue({});
|
update = vi.fn().mockResolvedValue({});
|
||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
|
|
||||||
vi.mock('../users/users.repository.js', () => ({
|
vi.mock('../../../src/modules/users/users.repository.js', () => ({
|
||||||
UsersRepository: class {
|
UsersRepository: class {
|
||||||
findByKeycloakId = vi.fn().mockResolvedValue({ householdIds: ['hh1'] });
|
findByKeycloakId = vi.fn().mockResolvedValue({ householdIds: ['hh1'] });
|
||||||
upsertFromToken = vi.fn().mockResolvedValue({ householdIds: ['hh1'] });
|
upsertFromToken = vi.fn().mockResolvedValue({ householdIds: ['hh1'] });
|
||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
|
|
||||||
import authPlugin from '../../plugins/auth.plugin.js';
|
import authPlugin from '../../../src/plugins/auth.plugin.js';
|
||||||
import householdPlugin from '../../plugins/household.plugin.js';
|
import householdPlugin from '../../../src/plugins/household.plugin.js';
|
||||||
import usersRoutes from '../users/users.routes.js';
|
import usersRoutes from '../../../src/modules/users/users.routes.js';
|
||||||
import shoppingListsRoutes from './shopping-lists.routes.js';
|
import shoppingListsRoutes from '../../../src/modules/shopping-lists/shopping-lists.routes.js';
|
||||||
|
|
||||||
describe('shopping-lists.routes', () => {
|
describe('shopping-lists.routes', () => {
|
||||||
let app: any;
|
let app: any;
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||||
import { ShoppingListsService } from './shopping-lists.service.js';
|
import { ShoppingListsService } from '../../../src/modules/shopping-lists/shopping-lists.service.js';
|
||||||
import { NotFoundError } from '../../common/errors.js';
|
import { NotFoundError } from '../../../src/common/errors.js';
|
||||||
|
|
||||||
describe('ShoppingListsService', () => {
|
describe('ShoppingListsService', () => {
|
||||||
let service: ShoppingListsService;
|
let service: ShoppingListsService;
|
||||||
|
|
@ -7,7 +7,7 @@ const { mockFind, mockFindOne, mockFindOneAndUpdate, mockSave } = vi.hoisted(()
|
||||||
mockSave: vi.fn(),
|
mockSave: vi.fn(),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
vi.mock('../../schemas/store.schema.js', () => {
|
vi.mock('../../../src/schemas/store.schema.js', () => {
|
||||||
const chain = () => ({
|
const chain = () => ({
|
||||||
sort: vi.fn().mockReturnThis(),
|
sort: vi.fn().mockReturnThis(),
|
||||||
limit: vi.fn().mockReturnThis(),
|
limit: vi.fn().mockReturnThis(),
|
||||||
|
|
@ -33,7 +33,7 @@ vi.mock('../../schemas/store.schema.js', () => {
|
||||||
return { StoreModel: FakeModel };
|
return { StoreModel: FakeModel };
|
||||||
});
|
});
|
||||||
|
|
||||||
import { StoresRepository } from './stores.repository.js';
|
import { StoresRepository } from '../../../src/modules/stores/stores.repository.js';
|
||||||
|
|
||||||
describe(StoresRepository.name, () => {
|
describe(StoresRepository.name, () => {
|
||||||
let repo: StoresRepository;
|
let repo: StoresRepository;
|
||||||
|
|
@ -26,7 +26,7 @@ const { mockList, mockGetById, mockCreate, mockUpdate, mockDeactivate } = vi.hoi
|
||||||
mockDeactivate: vi.fn(),
|
mockDeactivate: vi.fn(),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
vi.mock('./stores.repository.js', () => ({
|
vi.mock('../../../src/modules/stores/stores.repository.js', () => ({
|
||||||
StoresRepository: class {
|
StoresRepository: class {
|
||||||
findByHousehold = vi.fn();
|
findByHousehold = vi.fn();
|
||||||
findById = vi.fn();
|
findById = vi.fn();
|
||||||
|
|
@ -36,7 +36,7 @@ vi.mock('./stores.repository.js', () => ({
|
||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
|
|
||||||
vi.mock('./stores.service.js', () => ({
|
vi.mock('../../../src/modules/stores/stores.service.js', () => ({
|
||||||
StoresService: class {
|
StoresService: class {
|
||||||
list = mockList;
|
list = mockList;
|
||||||
getById = mockGetById;
|
getById = mockGetById;
|
||||||
|
|
@ -46,17 +46,17 @@ vi.mock('./stores.service.js', () => ({
|
||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
|
|
||||||
vi.mock('../users/users.repository.js', () => ({
|
vi.mock('../../../src/modules/users/users.repository.js', () => ({
|
||||||
UsersRepository: class {
|
UsersRepository: class {
|
||||||
findByKeycloakId = vi.fn().mockResolvedValue({ householdIds: ['hh1'] });
|
findByKeycloakId = vi.fn().mockResolvedValue({ householdIds: ['hh1'] });
|
||||||
upsertFromToken = vi.fn().mockResolvedValue({ householdIds: ['hh1'] });
|
upsertFromToken = vi.fn().mockResolvedValue({ householdIds: ['hh1'] });
|
||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
|
|
||||||
import authPlugin from '../../plugins/auth.plugin.js';
|
import authPlugin from '../../../src/plugins/auth.plugin.js';
|
||||||
import householdPlugin from '../../plugins/household.plugin.js';
|
import householdPlugin from '../../../src/plugins/household.plugin.js';
|
||||||
import usersRoutes from '../users/users.routes.js';
|
import usersRoutes from '../../../src/modules/users/users.routes.js';
|
||||||
import storesRoutes from './stores.routes.js';
|
import storesRoutes from '../../../src/modules/stores/stores.routes.js';
|
||||||
|
|
||||||
function makeFakeStore(overrides = {}) {
|
function makeFakeStore(overrides = {}) {
|
||||||
return {
|
return {
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||||
import { StoresService } from './stores.service.js';
|
import { StoresService } from '../../../src/modules/stores/stores.service.js';
|
||||||
|
|
||||||
describe(StoresService.name, () => {
|
describe(StoresService.name, () => {
|
||||||
const mockRepo = {
|
const mockRepo = {
|
||||||
|
|
@ -15,7 +15,7 @@ const { mockLean, mockExec, mockFindOne, mockFindById, mockFindOneAndUpdate, moc
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
vi.mock('../../schemas/user.schema.js', () => {
|
vi.mock('../../../src/schemas/user.schema.js', () => {
|
||||||
class MockUserModel {
|
class MockUserModel {
|
||||||
_data: Record<string, unknown>;
|
_data: Record<string, unknown>;
|
||||||
constructor(data: Record<string, unknown>) {
|
constructor(data: Record<string, unknown>) {
|
||||||
|
|
@ -36,7 +36,7 @@ vi.mock('../../schemas/user.schema.js', () => {
|
||||||
return { UserModel: MockUserModel };
|
return { UserModel: MockUserModel };
|
||||||
});
|
});
|
||||||
|
|
||||||
import { UsersRepository } from './users.repository.js';
|
import { UsersRepository } from '../../../src/modules/users/users.repository.js';
|
||||||
|
|
||||||
describe('UsersRepository', () => {
|
describe('UsersRepository', () => {
|
||||||
let repo: UsersRepository;
|
let repo: UsersRepository;
|
||||||
|
|
@ -24,15 +24,15 @@ const { mockUpsertFromToken, mockFindByKeycloakId } = vi.hoisted(() => ({
|
||||||
mockUpsertFromToken: vi.fn(),
|
mockUpsertFromToken: vi.fn(),
|
||||||
mockFindByKeycloakId: vi.fn(),
|
mockFindByKeycloakId: vi.fn(),
|
||||||
}));
|
}));
|
||||||
vi.mock('./users.repository.js', () => ({
|
vi.mock('../../../src/modules/users/users.repository.js', () => ({
|
||||||
UsersRepository: class MockUsersRepository {
|
UsersRepository: class MockUsersRepository {
|
||||||
upsertFromToken = mockUpsertFromToken;
|
upsertFromToken = mockUpsertFromToken;
|
||||||
findByKeycloakId = mockFindByKeycloakId;
|
findByKeycloakId = mockFindByKeycloakId;
|
||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
|
|
||||||
import authPlugin from '../../plugins/auth.plugin.js';
|
import authPlugin from '../../../src/plugins/auth.plugin.js';
|
||||||
import usersRoutes from './users.routes.js';
|
import usersRoutes from '../../../src/modules/users/users.routes.js';
|
||||||
|
|
||||||
describe('users.routes', () => {
|
describe('users.routes', () => {
|
||||||
async function buildTestApp() {
|
async function buildTestApp() {
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||||
import { UsersService } from './users.service.js';
|
import { UsersService } from '../../../src/modules/users/users.service.js';
|
||||||
import { NotFoundError } from '../../common/errors.js';
|
import { NotFoundError } from '../../../src/common/errors.js';
|
||||||
|
|
||||||
describe('UsersService', () => {
|
describe('UsersService', () => {
|
||||||
const mockRepo = {
|
const mockRepo = {
|
||||||
|
|
@ -19,7 +19,7 @@ const { mockFindByKeycloakId, mockUpsertFromToken } = vi.hoisted(() => ({
|
||||||
mockUpsertFromToken: vi.fn(),
|
mockUpsertFromToken: vi.fn(),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
import authPlugin from './auth.plugin.js';
|
import authPlugin from '../../src/plugins/auth.plugin.js';
|
||||||
import * as jose from 'jose';
|
import * as jose from 'jose';
|
||||||
|
|
||||||
describe('auth.plugin', () => {
|
describe('auth.plugin', () => {
|
||||||
|
|
@ -22,8 +22,8 @@ const { mockFindByKeycloakId } = vi.hoisted(() => ({
|
||||||
mockFindByKeycloakId: vi.fn(),
|
mockFindByKeycloakId: vi.fn(),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
import authPlugin from './auth.plugin.js';
|
import authPlugin from '../../src/plugins/auth.plugin.js';
|
||||||
import householdPlugin from './household.plugin.js';
|
import householdPlugin from '../../src/plugins/household.plugin.js';
|
||||||
|
|
||||||
describe('household.plugin', () => {
|
describe('household.plugin', () => {
|
||||||
async function buildApp() {
|
async function buildApp() {
|
||||||
|
|
@ -15,7 +15,7 @@ vi.mock('@fastify/awilix', () => ({
|
||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
|
|
||||||
import mongoosePlugin from './mongoose.plugin.js';
|
import mongoosePlugin from '../../src/plugins/mongoose.plugin.js';
|
||||||
import mongoose from 'mongoose';
|
import mongoose from 'mongoose';
|
||||||
import { diContainer } from '@fastify/awilix';
|
import { diContainer } from '@fastify/awilix';
|
||||||
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import { describe, it, expect } from 'vitest';
|
import { describe, it, expect } from 'vitest';
|
||||||
import { HouseholdModel } from './household.schema.js';
|
import { HouseholdModel } from '../../src/schemas/household.schema.js';
|
||||||
|
|
||||||
describe('HouseholdModel', () => {
|
describe('HouseholdModel', () => {
|
||||||
it('is a valid mongoose model', () => {
|
it('is a valid mongoose model', () => {
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import { describe, it, expect } from 'vitest';
|
import { describe, it, expect } from 'vitest';
|
||||||
import { MedicineProductModel } from './medicine-product.schema.js';
|
import { MedicineProductModel } from '../../src/schemas/medicine-product.schema.js';
|
||||||
|
|
||||||
describe(MedicineProductModel.name, () => {
|
describe(MedicineProductModel.name, () => {
|
||||||
it('is a valid mongoose model', () => {
|
it('is a valid mongoose model', () => {
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import { describe, it, expect } from 'vitest';
|
import { describe, it, expect } from 'vitest';
|
||||||
import { MedicineModel } from './medicine.schema.js';
|
import { MedicineModel } from '../../src/schemas/medicine.schema.js';
|
||||||
|
|
||||||
describe(MedicineModel.name, () => {
|
describe(MedicineModel.name, () => {
|
||||||
it('is a valid mongoose model', () => {
|
it('is a valid mongoose model', () => {
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import { describe, it, expect } from 'vitest';
|
import { describe, it, expect } from 'vitest';
|
||||||
import { ProductModel } from './product.schema.js';
|
import { ProductModel } from '../../src/schemas/product.schema.js';
|
||||||
|
|
||||||
describe(ProductModel.name, () => {
|
describe(ProductModel.name, () => {
|
||||||
it('is a valid mongoose model', () => {
|
it('is a valid mongoose model', () => {
|
||||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue