diff --git a/.github/workflows/pull-request-check.yml b/.github/workflows/pull-request-check.yml index 3bb61fb..c5e6505 100644 --- a/.github/workflows/pull-request-check.yml +++ b/.github/workflows/pull-request-check.yml @@ -1,8 +1,6 @@ name: Pull request to main +permissions: {} env: - NODE_VERSION: 21.7.3 - PNPM_VERSION: 10.6.0 - BIOME_VERSION: 1.9.3 DATABASE_PASSWORD: pass123 DATABASE_USER: postgres DATABASE_HOST: localhost @@ -25,39 +23,23 @@ jobs: uses: actions/checkout@v4 - name: Setup Biome uses: biomejs/setup-biome@v2 - with: - version: ${{ env.BIOME_VERSION }} - name: Run Biome run: biome ci . build: runs-on: ubuntu-latest - services: - database: - image: postgres:latest - env: - POSTGRES_PASSWORD: ${{ env.DATABASE_PASSWORD }} - # Wait until postgres has started - options: >- - --health-cmd pg_isready - --health-interval 10s - --health-timeout 5s - --health-retries 5 - ports: - - 5432:5432 steps: - name: Checkout code uses: actions/checkout@v4 - name: Setup pnpm uses: pnpm/action-setup@v4 - with: - version: ${{ env.PNPM_VERSION }} - name: Setup node uses: actions/setup-node@v4 with: - node-version: ${{ env.NODE_VERSION }} cache: "pnpm" - name: Install pnpm run: pnpm install + - name: Docker compose + run: docker compose up -d - name: Generate migration files run: pnpm db:generate - name: Migrate database @@ -73,19 +55,6 @@ jobs: run: pnpm start & test: runs-on: ubuntu-latest - services: - database: - image: postgres:latest - env: - POSTGRES_PASSWORD: ${{ env.DATABASE_PASSWORD }} - # Wait until postgres has started - options: >- - --health-cmd pg_isready - --health-interval 10s - --health-timeout 5s - --health-retries 5 - ports: - - 5432:5432 steps: - name: Checkout code uses: actions/checkout@v4 @@ -100,6 +69,8 @@ jobs: cache: "pnpm" - name: Install pnpm run: pnpm install + - name: Docker compose + run: docker compose up -d - name: Generate migration files run: pnpm db:generate - name: Migrate database diff --git a/.gitignore b/.gitignore index 1af1320..f516ffb 100644 --- a/.gitignore +++ b/.gitignore @@ -3,4 +3,6 @@ node_modules db/migrations .vscode build -openapi-document.yaml \ No newline at end of file +openapi-document.yaml +#From docker compose database +db-data \ No newline at end of file diff --git a/README.md b/README.md index 321062d..b37982b 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,7 @@ # Vektorprogrammets API Kildekoden er på engelsk +For at testene skal fungere, bruk node version >=22 ## Folder structure diff --git a/db/tables/expenses.ts b/db/tables/expenses.ts index dcc1b50..105f11d 100644 --- a/db/tables/expenses.ts +++ b/db/tables/expenses.ts @@ -3,11 +3,11 @@ import { usersTable } from "@/db/tables/users"; import { relations } from "drizzle-orm"; import { boolean, - date, integer, numeric, serial, text, + timestamp, } from "drizzle-orm/pg-core"; export const expensesTable = mainSchema.table("expenses", { @@ -19,10 +19,10 @@ export const expensesTable = mainSchema.table("expenses", { description: text("description").notNull(), moneyAmount: numeric("moneyAmount", { scale: 2 }).notNull(), accountNumber: text("accountNumber").notNull(), - purchaseDate: date("purchaseDate", { mode: "date" }).notNull(), - submitDate: date("submitDate", { mode: "date" }).defaultNow().notNull(), + purchaseTime: timestamp("purchaseTime").notNull(), + submitTime: timestamp("submitTime").defaultNow().notNull(), isAccepted: boolean("isAccepted").default(true).notNull(), - handlingDate: date("handlingDate", { mode: "date" }), + handlingTime: timestamp("handlingTime"), }); export const expensesRelations = relations(expensesTable, ({ one }) => ({ diff --git a/db/tables/sponsors.ts b/db/tables/sponsors.ts index 3f556d5..de408f6 100644 --- a/db/tables/sponsors.ts +++ b/db/tables/sponsors.ts @@ -1,7 +1,7 @@ import { departmentsTable } from "@/db/tables/departments"; import { mainSchema } from "@/db/tables/schema"; import { relations } from "drizzle-orm"; -import { date, integer, serial, text } from "drizzle-orm/pg-core"; +import { integer, serial, text, timestamp } from "drizzle-orm/pg-core"; export const sponsorSizeEnum = mainSchema.enum("size", [ "small", @@ -13,8 +13,8 @@ export const sponsorsTable = mainSchema.table("sponsors", { id: serial("id").primaryKey(), name: text("name").notNull(), homePageUrl: text("homePageURL").notNull(), - startDate: date("startDate", { mode: "date" }).notNull(), - endDate: date("endDate", { mode: "date" }), + startTime: timestamp("startTime").notNull(), + endTime: timestamp("endTime"), size: sponsorSizeEnum("size").notNull(), spesificDepartmentId: integer("spesificDepartmentId").references( () => departmentsTable.id, diff --git a/db/tables/team-applications.ts b/db/tables/team-applications.ts index 79bec00..f99040a 100644 --- a/db/tables/team-applications.ts +++ b/db/tables/team-applications.ts @@ -1,6 +1,6 @@ import { mainSchema } from "@/db/tables/schema"; import { relations } from "drizzle-orm"; -import { date, integer, serial, text } from "drizzle-orm/pg-core"; +import { integer, serial, text, timestamp } from "drizzle-orm/pg-core"; import { fieldsOfStudyTable } from "@/db/tables/fields-of-study"; import { teamsTable } from "@/db/tables/teams"; @@ -19,7 +19,7 @@ export const teamApplicationsTable = mainSchema.table("teamApplications", { yearOfStudy: integer("yearOfStudy").notNull(), biography: text("biography").notNull(), phonenumber: text("phonenumber").notNull(), - submitDate: date("submitDate", { mode: "date" }).defaultNow().notNull(), + submitTime: timestamp("submitTime").defaultNow().notNull(), }); export const teamApplicationsRelations = relations( diff --git a/db/tables/teams.ts b/db/tables/teams.ts index bd91427..716da91 100644 --- a/db/tables/teams.ts +++ b/db/tables/teams.ts @@ -3,7 +3,7 @@ import { mainSchema } from "@/db/tables/schema"; import { teamApplicationsTable } from "@/db/tables/team-applications"; import { teamUsersTable } from "@/db/tables/users"; import { relations } from "drizzle-orm"; -import { boolean, date, serial, text } from "drizzle-orm/pg-core"; +import { boolean, serial, text, timestamp } from "drizzle-orm/pg-core"; import { integer } from "drizzle-orm/pg-core"; export const teamsTable = mainSchema.table("teams", { @@ -17,7 +17,7 @@ export const teamsTable = mainSchema.table("teams", { shortDescription: text("shortDescription").notNull(), acceptApplication: boolean("acceptApplication").notNull(), active: boolean("active").notNull(), - deadline: date("deadline", { mode: "date" }), + deadline: timestamp("deadline"), }); export const teamRelations = relations(teamsTable, ({ one, many }) => ({ diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..0db3e5b --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,11 @@ +services: + db: + image: postgres:latest + environment: + - POSTGRES_PASSWORD=${DATABASE_PASSWORD} + - POSTGRES_USER=${DATABASE_USER} + - POSTGRES_DB=${DATABASE_NAME} + ports: + - ${DATABASE_PORT}:5432 + volumes: + - ./db-data:/var/lib/postgresql/data \ No newline at end of file diff --git a/lib/global-variables.ts b/lib/global-variables.ts index 4f8fab1..4ee694c 100644 --- a/lib/global-variables.ts +++ b/lib/global-variables.ts @@ -1 +1,4 @@ export const MAX_TEXT_LENGTH = 2500 as const; +export const DEFAULT_QUERY_LIMIT = 10 as const; + +export const DEFAULT_QUERY_SORT_OPTION = "desc" as const; diff --git a/lib/time-parsers.ts b/lib/time-parsers.ts new file mode 100644 index 0000000..5858adc --- /dev/null +++ b/lib/time-parsers.ts @@ -0,0 +1,30 @@ +import { z } from "zod"; + +export const timeStringParser = z.union([z.string().date(), z.string().time()]); + +export const dateParser = z.date(); +export const toDateParser = z + .union([timeStringParser, z.date()]) + .pipe(z.coerce.date()) + .pipe(dateParser); + +export const datePeriodParser = z + .object({ + startDate: dateParser, + endDate: dateParser, + }) + .refine((datePeriod) => { + return datePeriod.startDate.getTime() <= datePeriod.endDate.getTime(); + }, "Invalid date period. StartDate must be before or equal to endDate."); + +export const toDatePeriodParser = z + .object({ + startDate: toDateParser, + endDate: toDateParser, + }) + .pipe(datePeriodParser); + +export const pastDateParser = z.date().max(new Date()); +export const futureDateParser = z.date().min(new Date()); + +export type DatePeriod = z.infer; diff --git a/package.json b/package.json index a1c7821..9407c6c 100644 --- a/package.json +++ b/package.json @@ -10,12 +10,12 @@ "build": "tsc --project ./tsconfig.json && tsc-alias --project ./tsconfig.json --resolve-full-paths", "start": "node ./build/src/main.js", "prod": "pnpm build && pnpm start", - "test": "tsc --noEmit && node --import tsx --test ./src/test/*.ts", + "test": "tsc --noEmit && node --import tsx --test --test-force-exit ./src/test/*.ts", "format": "biome format --write", "lint": "biome lint --write", "check": "biome check --write", "db:generate": "drizzle-kit generate --config=db/config/drizzle.config.ts", - "db:migrate": "tsx ./db/setup/create-database.ts && drizzle-kit migrate --config=db/config/drizzle.config.ts", + "db:migrate": "drizzle-kit migrate --config=db/config/drizzle.config.ts", "db:studio": "drizzle-kit studio --config=db/config/drizzle.config.ts", "docs:generate": "tsx ./src/openapi/generateDocument.ts" }, @@ -40,13 +40,16 @@ "@types/express": "^4.17.21", "@types/node": "^22.7.8", "@types/pg": "^8.11.10", + "@types/supertest": "^6.0.2", "@types/swagger-jsdoc": "^6.0.4", "@types/swagger-ui-express": "^4.1.7", "@types/validator": "^13.12.2", "drizzle-kit": "^0.24.2", + "supertest": "^7.0.0", "tsc-alias": "^1.8.10", "tsx": "^4.19.1", "typescript": "^5.6.3", "yaml": "^2.6.0" - } -} \ No newline at end of file + }, + "packageManager": "pnpm@10.6.3+sha512.bb45e34d50a9a76e858a95837301bfb6bd6d35aea2c5d52094fa497a467c43f5c440103ce2511e9e0a2f89c3d6071baac3358fc68ac6fb75e2ceb3d2736065e6" +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 53adc7b..56dfdb0 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -60,6 +60,9 @@ importers: '@types/pg': specifier: ^8.11.10 version: 8.11.11 + '@types/supertest': + specifier: ^6.0.2 + version: 6.0.2 '@types/swagger-jsdoc': specifier: ^6.0.4 version: 6.0.4 @@ -72,6 +75,9 @@ importers: drizzle-kit: specifier: ^0.24.2 version: 0.24.2 + supertest: + specifier: ^7.0.0 + version: 7.0.0 tsc-alias: specifier: ^1.8.10 version: 1.8.11 @@ -610,6 +616,9 @@ packages: '@types/connect@3.4.38': resolution: {integrity: sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==} + '@types/cookiejar@2.1.5': + resolution: {integrity: sha512-he+DHOWReW0nghN24E1WUqM0efK4kI9oTqDm6XmK8ZPe2djZ90BSNdGnIyCLzCPw7/pogPlGbzI2wHGGmi4O/Q==} + '@types/cors@2.8.17': resolution: {integrity: sha512-8CGDvrBj1zgo2qE+oS3pOCyYNqCPryMWY2bGfwA0dcfopWGgxs+78df0Rs3rc9THP4JkOhLsAa+15VdpAqkcUA==} @@ -625,6 +634,9 @@ packages: '@types/json-schema@7.0.15': resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==} + '@types/methods@1.1.4': + resolution: {integrity: sha512-ymXWVrDiCxTBE3+RIrrP533E70eA+9qu7zdWoHuOmGujkYtzf4HQF96b8nwHLqhuf4ykX61IGRIB38CC6/sImQ==} + '@types/mime@1.3.5': resolution: {integrity: sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==} @@ -646,6 +658,12 @@ packages: '@types/serve-static@1.15.7': resolution: {integrity: sha512-W8Ym+h8nhuRwaKPaDw34QUkwsGi6Rc4yYqvKFo5rm2FUEhCFbzVWrxXUxuKK8TASjWsysJY0nsmNCGhCOIsrOw==} + '@types/superagent@8.1.9': + resolution: {integrity: sha512-pTVjI73witn+9ILmoJdajHGW2jkSaOzhiFYF1Rd3EQ94kymLqB9PjD9ISg7WaALC7+dCHT0FGe9T2LktLq/3GQ==} + + '@types/supertest@6.0.2': + resolution: {integrity: sha512-137ypx2lk/wTQbW6An6safu9hXmajAifU/s7szAHLN/FeIm5w7yR0Wkl9fdJMRSHwOn4HLAI0DaB2TOORuhPDg==} + '@types/swagger-jsdoc@6.0.4': resolution: {integrity: sha512-W+Xw5epcOZrF/AooUM/PccNMSAFOKWZA5dasNyMujTwsBkU74njSJBpvCCJhHAJ95XRMzQrrW844Btu0uoetwQ==} @@ -673,6 +691,12 @@ packages: resolution: {integrity: sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==} engines: {node: '>=8'} + asap@2.0.6: + resolution: {integrity: sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA==} + + asynckit@0.4.0: + resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==} + balanced-match@1.0.2: resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} @@ -713,6 +737,10 @@ packages: resolution: {integrity: sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==} engines: {node: '>= 8.10.0'} + combined-stream@1.0.8: + resolution: {integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==} + engines: {node: '>= 0.8'} + commander@6.2.0: resolution: {integrity: sha512-zP4jEKbe8SHzKJYQmq8Y9gYjtO/POJLgIdKgV7B9qNmABVFVc+ctqSX6iXh4mCpJfRBOabiZ2YKPg8ciDw6C+Q==} engines: {node: '>= 6'} @@ -721,6 +749,9 @@ packages: resolution: {integrity: sha512-KRs7WVDKg86PWiuAqhDrAQnTXZKraVcCc6vFdL14qrZ/DcWwuRo7VoiYXalXO7S5GKpqYiVEwCbgFDfxNHKJBQ==} engines: {node: ^12.20.0 || >=14} + component-emitter@1.3.1: + resolution: {integrity: sha512-T0+barUSQRTUQASh8bx02dl+DhF54GtIDY13Y3m9oWTklKbb3Wv974meRpeZ3lp1JpLVECWWNHC4vaG2XHXouQ==} + concat-map@0.0.1: resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} @@ -739,6 +770,9 @@ packages: resolution: {integrity: sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w==} engines: {node: '>= 0.6'} + cookiejar@2.1.4: + resolution: {integrity: sha512-LDx6oHrK+PhzLKJU9j5S7/Y3jM/mUHvD/DeI1WQmJn652iPC5Y4TBzC9l+5OMOXlyTTA+SmVUPm0HQUwpD5Jqw==} + cors@2.8.5: resolution: {integrity: sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==} engines: {node: '>= 0.10'} @@ -760,6 +794,10 @@ packages: supports-color: optional: true + delayed-stream@1.0.0: + resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==} + engines: {node: '>=0.4.0'} + depd@2.0.0: resolution: {integrity: sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==} engines: {node: '>= 0.8'} @@ -768,6 +806,9 @@ packages: resolution: {integrity: sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==} engines: {node: '>= 0.8', npm: 1.2.8000 || >= 1.4.16} + dezalgo@1.0.4: + resolution: {integrity: sha512-rXSP0bf+5n0Qonsb+SVVfNfIsimO4HEtmnIpPHY8Q1UCzKlQrDMfdobr8nJOOsRgWCyMRqeSBQzmWUMq7zvVig==} + dir-glob@3.0.1: resolution: {integrity: sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==} engines: {node: '>=8'} @@ -906,6 +947,10 @@ packages: resolution: {integrity: sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==} engines: {node: '>= 0.4'} + es-set-tostringtag@2.1.0: + resolution: {integrity: sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==} + engines: {node: '>= 0.4'} + esbuild-register@3.6.0: resolution: {integrity: sha512-H2/S7Pm8a9CL1uhp9OvjwrBh5Pvx0H8qVOxNu8Wed9Y7qv56MPtq+GGM8RJpq6glYJn9Wspr8uw7l55uyinNeg==} peerDependencies: @@ -945,6 +990,9 @@ packages: resolution: {integrity: sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==} engines: {node: '>=8.6.0'} + fast-safe-stringify@2.1.1: + resolution: {integrity: sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA==} + fastq@1.19.1: resolution: {integrity: sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==} @@ -956,6 +1004,13 @@ packages: resolution: {integrity: sha512-6BN9trH7bp3qvnrRyzsBz+g3lZxTNZTbVO2EV1CS0WIcDbawYVdYvGflME/9QP0h0pYlCDBCTjYa9nZzMDpyxQ==} engines: {node: '>= 0.8'} + form-data@4.0.2: + resolution: {integrity: sha512-hGfm/slu0ZabnNt4oaRZ6uREyfCj6P4fT/n6A1rGV+Z0VdGXjfOhVUpkn6qVQONHGIFwmveGXyDs75+nr6FM8w==} + engines: {node: '>= 6'} + + formidable@3.5.2: + resolution: {integrity: sha512-Jqc1btCy3QzRbJaICGwKcBfGWuLADRerLzDqi2NwSt/UkXLsHJw2TVResiaoBufHVHy9aSgClOHCeJsSsFLTbg==} + forwarded@0.2.0: resolution: {integrity: sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==} engines: {node: '>= 0.6'} @@ -1006,10 +1061,18 @@ packages: resolution: {integrity: sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==} engines: {node: '>= 0.4'} + has-tostringtag@1.0.2: + resolution: {integrity: sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==} + engines: {node: '>= 0.4'} + hasown@2.0.2: resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==} engines: {node: '>= 0.4'} + hexoid@2.0.0: + resolution: {integrity: sha512-qlspKUK7IlSQv2o+5I7yhUd7TxlOG2Vr5LTa3ve2XSNVKAL/n/u/7KLvKmFNimomDIKvZFXWHv0T12mv7rT8Aw==} + engines: {node: '>=8'} + http-errors@2.0.0: resolution: {integrity: sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==} engines: {node: '>= 0.8'} @@ -1100,6 +1163,11 @@ packages: engines: {node: '>=4'} hasBin: true + mime@2.6.0: + resolution: {integrity: sha512-USPkMeET31rOMiarsBNIHZKLGgvKc/LrjofAnBlOttf5ajRvqiRA8QsenbcooctK6d6Ts6aqZXBA+XbkKthiQg==} + engines: {node: '>=4.0.0'} + hasBin: true + minimatch@3.1.2: resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==} @@ -1331,6 +1399,14 @@ packages: resolution: {integrity: sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==} engines: {node: '>= 0.8'} + superagent@9.0.2: + resolution: {integrity: sha512-xuW7dzkUpcJq7QnhOsnNUgtYp3xRwpt2F7abdRYIpCsAt0hhUqia0EdxyXZQQpNmGtsCzYHryaKSV3q3GJnq7w==} + engines: {node: '>=14.18.0'} + + supertest@7.0.0: + resolution: {integrity: sha512-qlsr7fIC0lSddmA3tzojvzubYxvlGtzumcdHgPwbFWMISQwL22MhM2Y3LNt+6w9Yyx7559VW5ab70dgphm8qQA==} + engines: {node: '>=14.18.0'} + swagger-jsdoc@6.2.8: resolution: {integrity: sha512-VPvil1+JRpmJ55CgAtn8DIcpBs0bL5L3q5bVQvF4tAW/k/9JYSj7dCpaYCAv5rufe0vcCbBRQXGvzpkWjvLklQ==} engines: {node: '>=12.0.0'} @@ -1735,6 +1811,8 @@ snapshots: dependencies: '@types/node': 22.13.9 + '@types/cookiejar@2.1.5': {} + '@types/cors@2.8.17': dependencies: '@types/node': 22.13.9 @@ -1757,6 +1835,8 @@ snapshots: '@types/json-schema@7.0.15': {} + '@types/methods@1.1.4': {} + '@types/mime@1.3.5': {} '@types/node@22.13.9': @@ -1784,6 +1864,18 @@ snapshots: '@types/node': 22.13.9 '@types/send': 0.17.4 + '@types/superagent@8.1.9': + dependencies: + '@types/cookiejar': 2.1.5 + '@types/methods': 1.1.4 + '@types/node': 22.13.9 + form-data: 4.0.2 + + '@types/supertest@6.0.2': + dependencies: + '@types/methods': 1.1.4 + '@types/superagent': 8.1.9 + '@types/swagger-jsdoc@6.0.4': {} '@types/swagger-ui-express@4.1.8': @@ -1809,6 +1901,10 @@ snapshots: array-union@2.1.0: {} + asap@2.0.6: {} + + asynckit@0.4.0: {} + balanced-match@1.0.2: {} binary-extensions@2.3.0: {} @@ -1867,10 +1963,16 @@ snapshots: optionalDependencies: fsevents: 2.3.3 + combined-stream@1.0.8: + dependencies: + delayed-stream: 1.0.0 + commander@6.2.0: {} commander@9.5.0: {} + component-emitter@1.3.1: {} + concat-map@0.0.1: {} content-disposition@0.5.4: @@ -1883,6 +1985,8 @@ snapshots: cookie@0.7.1: {} + cookiejar@2.1.4: {} + cors@2.8.5: dependencies: object-assign: 4.1.1 @@ -1896,10 +2000,17 @@ snapshots: dependencies: ms: 2.1.3 + delayed-stream@1.0.0: {} + depd@2.0.0: {} destroy@1.2.0: {} + dezalgo@1.0.4: + dependencies: + asap: 2.0.6 + wrappy: 1.0.2 + dir-glob@3.0.1: dependencies: path-type: 4.0.0 @@ -1949,6 +2060,13 @@ snapshots: dependencies: es-errors: 1.3.0 + es-set-tostringtag@2.1.0: + dependencies: + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + has-tostringtag: 1.0.2 + hasown: 2.0.2 + esbuild-register@3.6.0(esbuild@0.19.12): dependencies: debug: 4.4.0 @@ -2085,6 +2203,8 @@ snapshots: merge2: 1.4.1 micromatch: 4.0.8 + fast-safe-stringify@2.1.1: {} + fastq@1.19.1: dependencies: reusify: 1.1.0 @@ -2105,6 +2225,19 @@ snapshots: transitivePeerDependencies: - supports-color + form-data@4.0.2: + dependencies: + asynckit: 0.4.0 + combined-stream: 1.0.8 + es-set-tostringtag: 2.1.0 + mime-types: 2.1.35 + + formidable@3.5.2: + dependencies: + dezalgo: 1.0.4 + hexoid: 2.0.0 + once: 1.4.0 + forwarded@0.2.0: {} fresh@0.5.2: {} @@ -2164,10 +2297,16 @@ snapshots: has-symbols@1.1.0: {} + has-tostringtag@1.0.2: + dependencies: + has-symbols: 1.1.0 + hasown@2.0.2: dependencies: function-bind: 1.1.2 + hexoid@2.0.0: {} + http-errors@2.0.0: dependencies: depd: 2.0.0 @@ -2236,6 +2375,8 @@ snapshots: mime@1.6.0: {} + mime@2.6.0: {} + minimatch@3.1.2: dependencies: brace-expansion: 1.1.11 @@ -2457,6 +2598,27 @@ snapshots: statuses@2.0.1: {} + superagent@9.0.2: + dependencies: + component-emitter: 1.3.1 + cookiejar: 2.1.4 + debug: 4.4.0 + fast-safe-stringify: 2.1.1 + form-data: 4.0.2 + formidable: 3.5.2 + methods: 1.1.2 + mime: 2.6.0 + qs: 6.13.0 + transitivePeerDependencies: + - supports-color + + supertest@7.0.0: + dependencies: + methods: 1.1.2 + superagent: 9.0.2 + transitivePeerDependencies: + - supports-color + swagger-jsdoc@6.2.8(openapi-types@12.1.3): dependencies: commander: 6.2.0 diff --git a/src/db-access/expenses.ts b/src/db-access/expenses.ts index 7fbc871..b7b3201 100644 --- a/src/db-access/expenses.ts +++ b/src/db-access/expenses.ts @@ -1,15 +1,13 @@ import { database } from "@/db/setup/query-postgres"; import { expensesTable } from "@/db/tables/expenses"; +import type { DatePeriod } from "@/lib/time-parsers"; import { type OrmResult, handleDatabaseFullfillment, handleDatabaseRejection, ormError, } from "@/src/error/orm-error"; -import type { - DatePeriod, - QueryParameters, -} from "@/src/request-handling/common"; +import type { QueryParameters } from "@/src/request-handling/common"; import type { NewExpense } from "@/src/request-handling/expenses"; import type { Expense, ExpenseKey } from "@/src/response-handling/expenses"; import { @@ -46,7 +44,7 @@ export async function paybackExpenses( .from(expensesTable) .where( and( - isNotNull(expensesTable.handlingDate), + isNotNull(expensesTable.handlingTime), inArray(expensesTable.id, expenseIds), ), ); @@ -56,7 +54,7 @@ export async function paybackExpenses( const updateResult = await database .update(expensesTable) - .set({ handlingDate: new Date(), isAccepted: true }) + .set({ handlingTime: new Date(), isAccepted: true }) .where(inArray(expensesTable.id, expenseIds)) .returning(); if (updateResult.length !== expenseIds.length) { @@ -77,7 +75,7 @@ export async function rejectExpense( .from(expensesTable) .where( and( - isNotNull(expensesTable.handlingDate), + isNotNull(expensesTable.handlingTime), inArray(expensesTable.id, expenseIds), ), ); @@ -87,7 +85,7 @@ export async function rejectExpense( const updateResult = await database .update(expensesTable) - .set({ handlingDate: new Date(), isAccepted: false }) + .set({ handlingTime: new Date(), isAccepted: false }) .where(inArray(expensesTable.id, expenseIds)) .returning(); if (updateResult.length !== expenseIds.length) { @@ -139,9 +137,9 @@ export async function getSumUnprocessed( .from(expensesTable) .where( and( - isNull(expensesTable.handlingDate), + isNull(expensesTable.handlingTime), between( - expensesTable.submitDate, + expensesTable.submitTime, timePeriod.startDate, timePeriod.endDate, ), @@ -175,9 +173,9 @@ export async function getSumAccepted( .where( and( expensesTable.isAccepted, - isNotNull(expensesTable.handlingDate), + isNotNull(expensesTable.handlingTime), between( - expensesTable.submitDate, + expensesTable.submitTime, timePeriod.startDate, timePeriod.endDate, ), @@ -211,9 +209,9 @@ export async function getSumRejected( .where( and( not(expensesTable.isAccepted), - isNotNull(expensesTable.handlingDate), + isNotNull(expensesTable.handlingTime), between( - expensesTable.submitDate, + expensesTable.submitTime, timePeriod.startDate, timePeriod.endDate, ), @@ -246,9 +244,9 @@ export async function getAveragePaybackTime( .from(expensesTable) .where( and( - isNotNull(expensesTable.handlingDate), + isNotNull(expensesTable.handlingTime), between( - expensesTable.submitDate, + expensesTable.submitTime, timePeriod.startDate, timePeriod.endDate, ), @@ -260,12 +258,12 @@ export async function getAveragePaybackTime( } const totalMilliseconds = result.reduce((accumulator, currentValue) => { - // handlingDate have already checked not to be null - const handlingDate = currentValue.handlingDate as Date; + // handlingTime have already checked not to be null + const handlingDate = currentValue.handlingTime as Date; return ( accumulator + - (handlingDate.getTime() - currentValue.submitDate.getTime()) + (handlingDate.getTime() - currentValue.submitTime.getTime()) ); }, 0); diff --git a/src/main.ts b/src/main.ts index b9efd53..6853a0e 100644 --- a/src/main.ts +++ b/src/main.ts @@ -1,14 +1,12 @@ import "dotenv/config"; -import express from "express"; - -import { hostOptions } from "@/src/enviroment"; import { defaultErrorHandler, errorHandler, } from "@/src/middleware/error-middleware"; import { logger } from "@/src/middleware/logging-middleware"; +import express from "express"; -import { expenseRouter, expensesRouter } from "@/src/routers/expenses"; +import { expensesRouter } from "@/src/routers/expenses"; import { customCors, customHelmetSecurity } from "@/src/security"; import { teamApplicationRouter } from "@/src/routers/team-applications"; @@ -17,33 +15,33 @@ import { openapiSpecification } from "@/src/openapi/config"; import { sponsorsRouter } from "@/src/routers/sponsors"; import { usersRouter } from "@/src/routers/users"; import openapiExpressHandler from "swagger-ui-express"; +import { hostOptions } from "./enviroment"; -const app = express(); +export const api = express(); // Security -app.use(customHelmetSecurity); -app.disable("x-powered-by"); -app.use(customCors()); +api.use(customHelmetSecurity); +api.disable("x-powered-by"); +api.use(customCors()); // OpenAPI -app.use("/docs/api", openapiExpressHandler.serve); -app.get("/docs/api", openapiExpressHandler.setup(openapiSpecification)); +api.use("/docs/api", openapiExpressHandler.serve); +api.get("/docs/api", openapiExpressHandler.setup(openapiSpecification)); -app.use("/", logger); +api.use("", logger); -app.use("/expense", expenseRouter); -app.use("/expenses", expensesRouter); +api.use("/expenses", expensesRouter); -app.use("/sponsors", sponsorsRouter); +api.use("/sponsors", sponsorsRouter); -app.use("/users", usersRouter); +api.use("/users", usersRouter); -app.use("/teamapplications", teamApplicationRouter); +api.use("/teamapplications", teamApplicationRouter); -app.use("", errorHandler); -app.use("", defaultErrorHandler); +api.use("", errorHandler); +api.use("", defaultErrorHandler); -app.listen(hostOptions.port, () => { +api.listen(hostOptions.port, () => { console.info( `Listening on ${hostOptions.hostingUrl}. May need to specify port ${hostOptions.port}.`, ); diff --git a/src/openapi/config.ts b/src/openapi/config.ts index b5ebea8..65cfe23 100644 --- a/src/openapi/config.ts +++ b/src/openapi/config.ts @@ -1,7 +1,7 @@ import "zod-openapi/extend"; +import { datePeriodParser } from "@/lib/time-parsers"; import { hostOptions } from "@/src/enviroment"; import { - datePeriodParser, limitParser, offsetParser, serialIdParser, diff --git a/src/request-handling/common.ts b/src/request-handling/common.ts index d0340df..32435cd 100644 --- a/src/request-handling/common.ts +++ b/src/request-handling/common.ts @@ -1,3 +1,4 @@ +import { DEFAULT_QUERY_LIMIT } from "@/lib/global-variables"; import { z } from "zod"; export const sortParser = z @@ -11,12 +12,13 @@ export const limitParser = z .safe() .positive() .int() - .default(10) + .default(DEFAULT_QUERY_LIMIT) .describe("Amount of items requested"); export const toLimitParser = z .union([z.number(), z.string()]) .pipe(z.coerce.number()) - .pipe(limitParser); + .pipe(limitParser) + .default(DEFAULT_QUERY_LIMIT); export const offsetParser = z .number() .finite() @@ -28,7 +30,8 @@ export const offsetParser = z export const toOffsetParser = z .union([z.number(), z.string()]) .pipe(z.coerce.number()) - .pipe(offsetParser); + .pipe(offsetParser) + .default(0); export const listQueryParser = z.object({ sort: sortParser, limit: limitParser, @@ -49,27 +52,3 @@ export const toSerialIdParser = z .union([z.number(), z.string()]) .pipe(z.coerce.number()) .pipe(serialIdParser); - -export const dateParser = z.date(); -export const toDateParser = z - .union([z.string().date(), z.date()]) - .pipe(z.coerce.date()) - .pipe(dateParser); - -export const datePeriodParser = z - .object({ - startDate: dateParser, - endDate: dateParser, - }) - .refine((datePeriod) => { - return datePeriod.startDate.getTime() <= datePeriod.endDate.getTime(); - }, "Invalid date period. StartDate must be before or equal to endDate."); - -export const toDatePeriodParser = z - .object({ - startDate: toDateParser, - endDate: toDateParser, - }) - .pipe(datePeriodParser); - -export type DatePeriod = z.infer; diff --git a/src/request-handling/expenses.ts b/src/request-handling/expenses.ts index 0976436..21d7d21 100644 --- a/src/request-handling/expenses.ts +++ b/src/request-handling/expenses.ts @@ -3,6 +3,7 @@ import { currencyParser, norwegianBankAccountNumberParser, } from "@/lib/finance-parsers"; +import { timeStringParser } from "@/lib/time-parsers"; import { serialIdParser } from "@/src/request-handling/common"; import { createInsertSchema } from "drizzle-zod"; import { z } from "zod"; @@ -17,10 +18,7 @@ export const expenseRequestParser = z .string() .length(11) .describe("Norwegian account number"), - purchaseDate: z - .string() - .date("Must be valid datestring (YYYY-MM-DD)") - .describe("Date of purcase"), + purchaseTime: timeStringParser.describe("Time of purcase"), }) .strict(); export const expenseRequestToInsertParser = expenseRequestParser @@ -30,7 +28,7 @@ export const expenseRequestToInsertParser = expenseRequestParser bankAccountNumber: expenseRequestParser.shape.bankAccountNumber.pipe( norwegianBankAccountNumberParser, ), - purchaseDate: expenseRequestParser.shape.purchaseDate.pipe( + purchaseTime: expenseRequestParser.shape.purchaseTime.pipe( z.coerce.date().max(new Date()), ), }) diff --git a/src/request-handling/sponsors.ts b/src/request-handling/sponsors.ts index 64a6850..5eab9cf 100644 --- a/src/request-handling/sponsors.ts +++ b/src/request-handling/sponsors.ts @@ -1,4 +1,5 @@ import { sponsorsTable } from "@/db/tables/sponsors"; +import { timeStringParser } from "@/lib/time-parsers"; import { serialIdParser } from "@/src/request-handling/common"; import { createInsertSchema } from "drizzle-zod"; import { z } from "zod"; @@ -8,13 +9,8 @@ export const sponsorRequestParser = z id: serialIdParser.describe("Id of sponsor"), name: z.string().describe("Name of sponsor"), homePageUrl: z.string().url().describe("URL to homepage of sponsor"), - startDate: z - .string() - .date("Must be valid datestring (YYYY-MM-DD") - .describe("Date when sponsor started support"), - endDate: z - .string() - .date("Must be valid datestring (YYYY-MM-DD") + startTime: timeStringParser.describe("Date when sponsor started support"), + endTime: timeStringParser .nullable() .describe("Date when sponsor ended support"), size: z @@ -29,10 +25,10 @@ export const sponsorRequestParser = z export const sponsorRequestToInsertParser = sponsorRequestParser .extend({ name: sponsorRequestParser.shape.name.trim(), - startDate: sponsorRequestParser.shape.startDate.pipe( + startTime: sponsorRequestParser.shape.startTime.pipe( z.coerce.date().max(new Date()), ), - endDate: sponsorRequestParser.shape.endDate.pipe(z.coerce.date()), + endTime: sponsorRequestParser.shape.endTime.pipe(z.coerce.date()), }) .pipe(createInsertSchema(sponsorsTable).strict().readonly()); diff --git a/src/routers/expenses.ts b/src/routers/expenses.ts index 5827911..38480d8 100644 --- a/src/routers/expenses.ts +++ b/src/routers/expenses.ts @@ -1,3 +1,4 @@ +import { toDatePeriodParser } from "@/lib/time-parsers"; import { getAveragePaybackTime, getSumAccepted, @@ -11,20 +12,18 @@ import { } from "@/src/db-access/expenses"; import { clientError } from "@/src/error/http-errors"; import { - toDatePeriodParser, toListQueryParser, toSerialIdParser, } from "@/src/request-handling/common"; import { expenseRequestToInsertParser } from "@/src/request-handling/expenses"; import { Router, json } from "express"; -export const expenseRouter = Router(); export const expensesRouter = Router(); -expenseRouter.use(json()); +expensesRouter.use(json()); /** * @openapi - * /expense/: + * /expenses: * post: * tags: [expenses] * summary: Add expense @@ -43,7 +42,7 @@ expenseRouter.use(json()); * schema: * $ref: "#/components/schemas/expense" */ -expenseRouter.post("/", async (req, res, next) => { +expensesRouter.post("", async (req, res, next) => { const expenseRequest = expenseRequestToInsertParser.safeParse(req.body); if (!expenseRequest.success) { const error = clientError( @@ -65,11 +64,9 @@ expenseRouter.post("/", async (req, res, next) => { res.status(201).json(databaseResult.data); }); -expensesRouter.use(json()); - /** * @openapi - * /expense/{id}/payback/: + * /expenses/{id}/payback: * put: * tags: [expenses] * summary: Payback expense with ID @@ -84,7 +81,7 @@ expensesRouter.use(json()); * schema: * $ref: "#/components/schemas/expense" */ -expenseRouter.put("/:id/payback/", async (req, res, next) => { +expensesRouter.put("/:id/payback", async (req, res, next) => { const paybackRequest = toSerialIdParser.safeParse(req.params.id); if (!paybackRequest.success) { return next( @@ -106,7 +103,7 @@ expenseRouter.put("/:id/payback/", async (req, res, next) => { /** * @openapi - * /expense/{id}/reject/: + * /expenses/{id}/reject: * put: * tags: [expenses] * summary: Reject expense with ID @@ -121,7 +118,7 @@ expenseRouter.put("/:id/payback/", async (req, res, next) => { * schema: * $ref: "#/components/schemas/expense" */ -expenseRouter.put("/:id/reject/", async (req, res, next) => { +expensesRouter.put("/:id/reject", async (req, res, next) => { const rejectRequest = toSerialIdParser.safeParse(req.params.id); if (!rejectRequest.success) { return next( @@ -143,7 +140,7 @@ expenseRouter.put("/:id/reject/", async (req, res, next) => { /** * @openapi - * /expense/{id}/: + * /expenses/{id}: * get: * tags: [expenses] * summary: Get expense with id @@ -158,7 +155,7 @@ expenseRouter.put("/:id/reject/", async (req, res, next) => { * schema: * $ref: "#/components/schemas/expense" */ -expenseRouter.get("/:expenseId/", async (req, res, next) => { +expensesRouter.get("/:expenseId", async (req, res, next) => { const expenseIdResult = toSerialIdParser.safeParse(req.params.expenseId); if (!expenseIdResult.success) { return next( @@ -180,7 +177,7 @@ expenseRouter.get("/:expenseId/", async (req, res, next) => { /** * @openapi - * /expenses/: + * /expenses: * get: * tags: [expenses] * summary: Get expenses @@ -197,7 +194,7 @@ expenseRouter.get("/:expenseId/", async (req, res, next) => { * schema: * $ref: "#/components/schemas/expense" */ -expensesRouter.get("/", async (req, res, next) => { +expensesRouter.get("", async (req, res, next) => { const queryParametersResult = toListQueryParser.safeParse(req.query); if (!queryParametersResult.success) { return next( @@ -219,7 +216,7 @@ expensesRouter.get("/", async (req, res, next) => { /** * @openapi - * /expenses/money-amount/unprocessed/: + * /expensess/money-amount/unprocessed: * get: * tags: [expenses] * requestBody: @@ -238,7 +235,7 @@ expensesRouter.get("/", async (req, res, next) => { * application/json: * schema: */ -expensesRouter.get("/money-amount/unprocessed/", async (req, res, next) => { +expensesRouter.get("/money-amount/unprocessed", async (req, res, next) => { const bodyParameterResult = toDatePeriodParser.safeParse(req.body); if (!bodyParameterResult.success) { return next( @@ -260,7 +257,7 @@ expensesRouter.get("/money-amount/unprocessed/", async (req, res, next) => { /** * @openapi - * /expenses/money-amount/accepted/: + * /expenses/money-amount/accepted: * get: * tags: [expenses] * requestBody: @@ -279,7 +276,7 @@ expensesRouter.get("/money-amount/unprocessed/", async (req, res, next) => { * application/json: * schema: */ -expensesRouter.get("/money-amount/accepted/", async (req, res, next) => { +expensesRouter.get("/money-amount/accepted", async (req, res, next) => { const bodyParameterResult = toDatePeriodParser.safeParse(req.body); if (!bodyParameterResult.success) { return next( @@ -301,7 +298,7 @@ expensesRouter.get("/money-amount/accepted/", async (req, res, next) => { /** * @openapi - * /expenses/money-amount/rejected/: + * /expenses/money-amount/rejected: * get: * tags: [expenses] * requestBody: @@ -320,7 +317,7 @@ expensesRouter.get("/money-amount/accepted/", async (req, res, next) => { * application/json: * schema: */ -expensesRouter.get("/money-amount/rejected/", async (req, res, next) => { +expensesRouter.get("/money-amount/rejected", async (req, res, next) => { const bodyParameterResult = toDatePeriodParser.safeParse(req.body); if (!bodyParameterResult.success) { return next( @@ -342,7 +339,7 @@ expensesRouter.get("/money-amount/rejected/", async (req, res, next) => { /** * @openapi - * /expenses/payback-time/average/: + * /expenses/payback-time/average: * get: * tags: [expenses] * requestBody: @@ -361,7 +358,7 @@ expensesRouter.get("/money-amount/rejected/", async (req, res, next) => { * application/json: * schema: */ -expensesRouter.get("/payback-time/average/", async (req, res, next) => { +expensesRouter.get("/payback-time/average", async (req, res, next) => { const bodyParameterResult = toDatePeriodParser.safeParse(req.body); if (!bodyParameterResult.success) { return next( diff --git a/src/test/example.ts b/src/test/example.ts deleted file mode 100644 index 47d21cb..0000000 --- a/src/test/example.ts +++ /dev/null @@ -1,6 +0,0 @@ -import assert from "node:assert/strict"; -import { test } from "node:test"; - -test("Example test", () => { - assert.strictEqual(1, 1); -}); diff --git a/src/test/expenses.ts b/src/test/expenses.ts new file mode 100644 index 0000000..22fad5e --- /dev/null +++ b/src/test/expenses.ts @@ -0,0 +1,56 @@ +import { test } from "node:test"; +import { api } from "@/src/main"; +import supertest from "supertest"; + +const jsonRegex = /json/; + +test("GET /expenses", async () => { + await supertest(api) + .get("/expenses") + .expect(200) + .expect("Content-Type", jsonRegex); +}); + +test("GET /expenses/money-amount/unprocessed", async () => { + await supertest(api) + .get("/expenses/money-amount/unprocessed") + .send({ + startDate: "1976-04-01", + endDate: "2025-02-01", + }) + .expect(200) + .expect("Content-Type", jsonRegex); +}); + +test("GET /expenses/money-amount/accepted", async () => { + await supertest(api) + .get("/expenses/money-amount/accepted") + .send({ + startDate: "1976-04-01", + endDate: "2025-02-01", + }) + .expect(200) + .expect("Content-Type", jsonRegex); +}); + +test("GET /expenses/money-amount/rejected", async () => { + await supertest(api) + .get("/expenses/money-amount/rejected") + .send({ + startDate: "1976-04-01", + endDate: "2025-02-01", + }) + .expect(200) + .expect("Content-Type", jsonRegex); +}); + +test("GET /expenses/payback-time/average", async () => { + await supertest(api) + .get("/expenses/payback-time/average") + .send({ + startDate: "1976-04-01", + endDate: "2025-02-01", + }) + .expect(200) + .expect("Content-Type", jsonRegex); +});