From c55fb8f74642ac2f29c587ea45e3e0cccef4ef35 Mon Sep 17 00:00:00 2001 From: Farhod Date: Wed, 4 Feb 2026 22:39:53 -0500 Subject: [PATCH] Complete banking app with API and UI tests --- README.md | 105 ++++++----- SPECS/banking-transactions.md | 27 +++ app/__pycache__/database.cpython-313.pyc | Bin 0 -> 1603 bytes app/__pycache__/main.cpython-313.pyc | Bin 0 -> 6988 bytes app/__pycache__/models.cpython-313.pyc | Bin 0 -> 1673 bytes app/database.py | 32 ++++ app/main.py | 108 ++++++++++++ app/models.py | 27 +++ banking.db | Bin 0 -> 16384 bytes frontend/index.html | 165 ++++++++++++++++++ requirements.txt | 7 + .../test_api.cpython-313-pytest-9.0.2.pyc | Bin 0 -> 25989 bytes .../test_ui.cpython-313-pytest-9.0.2.pyc | Bin 0 -> 4329 bytes tests/test_api.py | 149 ++++++++++++++++ tests/test_ui.py | 66 +++++++ 15 files changed, 643 insertions(+), 43 deletions(-) create mode 100644 SPECS/banking-transactions.md create mode 100644 app/__pycache__/database.cpython-313.pyc create mode 100644 app/__pycache__/main.cpython-313.pyc create mode 100644 app/__pycache__/models.cpython-313.pyc create mode 100644 app/database.py create mode 100644 app/main.py create mode 100644 app/models.py create mode 100644 banking.db create mode 100644 frontend/index.html create mode 100644 requirements.txt create mode 100644 tests/__pycache__/test_api.cpython-313-pytest-9.0.2.pyc create mode 100644 tests/__pycache__/test_ui.cpython-313-pytest-9.0.2.pyc create mode 100644 tests/test_api.py create mode 100644 tests/test_ui.py diff --git a/README.md b/README.md index 494f1c75..a0c5ad7f 100644 --- a/README.md +++ b/README.md @@ -1,43 +1,62 @@ -# Candidate Assessment: Spec-Driven Development With Codegen Tools - -This assessment evaluates how you use modern code generation tools (for example `5.2-Codex`, `Claude`, `Copilot`, and similar) to design, build, and test a software application using a spec-driven development pattern. You may build a frontend, a backend, or both. - -## Goals -- Build a working application with at least one meaningful feature. -- Create a testing framework to validate the application. -- Demonstrate effective use of code generation tools to accelerate delivery. -- Show clear, maintainable engineering practices. - -## Deliverables -- Application source code in this repository. -- A test suite and test harness that can be run locally. -- Documentation that explains how to run the app and the tests. - -## Scope Options -Pick one: -- Frontend-only application. -- Backend-only application. -- Full-stack application. - -Your solution should include at least one real workflow, for example: -- Create and view a resource. -- Search or filter data. -- Persist data in memory or storage. - -## Rules -- You must use a code generation tool (for example `5.2-Codex`, `Claude`, or similar). You can use multiple tools. -- You must build the application and a testing framework for it. -- The application and tests must run locally. -- Do not include secrets or credentials in this repository. - -## Evaluation Criteria -- Working product: Does the app do what it claims? -- Test coverage: Do tests cover key workflows and edge cases? -- Engineering quality: Clarity, structure, and maintainability. -- Use of codegen: How effectively you used tools to accelerate work. -- Documentation: Clear setup and run instructions. - -## What to Submit -- When you are complete, put up a Pull Request against this repository with your changes. -- A short summary of your approach and tools used in your PR submission -- Any additional information or approach that helped you. +# Banking Transaction System + +A full-stack banking application built for the spec-driven-development assessment. + +## Features +- Create bank accounts with initial balance +- Deposit and withdraw funds +- Transfer between accounts +- View transaction history +- Input validation and error handling + +## Tech Stack +- **Backend:** FastAPI (Python) +- **Database:** SQLite +- **Frontend:** HTML/CSS/JavaScript +- **Testing:** pytest, Playwright + +## Setup + +```bash +# Clone and navigate +git clone https://github.com/Farhod75/spec-driven-development.git +cd spec-driven-development + +# Create virtual environment +python -m venv venv +.\venv\Scripts\activate # Windows +# source venv/bin/activate # macOS/Linux + +# Install dependencies +pip install -r requirements.txt +playwright install chromium + +# Run the Application +uvicorn app.main:app --reload --port 8000 +Open http://localhost:8000 + +# Run Tests +# API tests +pytest tests/test_api.py -v + +# UI tests (server must be running) +pytest tests/test_ui.py -v + +# All tests +pytest -v + +API Endpoints +Method Endpoint Description +POST /accounts Create account +GET /accounts/{id} Get account details +POST /accounts/{id}/deposit Deposit funds +POST /accounts/{id}/withdraw Withdraw funds +POST /transfers Transfer between accounts +GET /accounts/{id}/transactions Get transaction history + +# AI Tools Used + +Claude (Anthropic) - Code generation and debugging + +# Author +Farhod Elbekov \ No newline at end of file diff --git a/SPECS/banking-transactions.md b/SPECS/banking-transactions.md new file mode 100644 index 00000000..d9316675 --- /dev/null +++ b/SPECS/banking-transactions.md @@ -0,0 +1,27 @@ +# Feature Spec: Banking Transactions + +## Goal +Build a simple banking system that allows users to create accounts, manage balances, and perform transactions. + +## Scope +- In: Account creation, deposits, withdrawals, transfers, transaction history, basic UI +- Out: Authentication, multi-currency, external integrations + +## Requirements +- Users can create bank accounts with initial balance +- Users can deposit money into accounts +- Users can withdraw money (with insufficient funds validation) +- Users can transfer between accounts +- Users can view transaction history +- API returns appropriate status codes and error messages + +## Acceptance Criteria +- [ ] POST /accounts creates a new account and returns account details +- [ ] GET /accounts/{id} returns account with current balance +- [ ] POST /accounts/{id}/deposit increases balance +- [ ] POST /accounts/{id}/withdraw decreases balance (fails if insufficient funds) +- [ ] POST /transfers moves money between accounts +- [ ] GET /accounts/{id}/transactions returns transaction history +- [ ] UI allows all operations above +- [ ] All API endpoints have passing tests +- [ ] UI has Playwright E2E tests \ No newline at end of file diff --git a/app/__pycache__/database.cpython-313.pyc b/app/__pycache__/database.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..6c0e99d8af0c77cd36b5b3e340c59b80b97aa39b GIT binary patch literal 1603 zcmb_c&2QX96o0n&E8gtVt{S?Es&;@<#Z?nUKm_8Wm78^{B^xK$Ua3_hMPBb@WwExG zv6D9RvO*kaDFh@UE}W5i<}b*ffT%=e3aSK`-U5pRLL7MG?6%pYO)oslZ{Ezj_vU@f zZ+eA-3`k44kKL~%fM5A!Kw@9$y@J9Pm|zO?!1yT63-5|dBw$KRGE?KgQp=*59*3E} z2}`#GCG(KabVZ1ZPT)uMdgR%@-JL(6$&f*gQ9#c}wgp#(WEI)K zuLwTW!QeXk$P{M!+89;x&Gh3Z(1I7>98B~rny9ymv7Z8hUkl!mPQj|6h`N0qEo8EX zW(`fXG-9b2YZ|G}5q;4j+U2TgnZ$OSV9k$0GBISqjol`x>Xvp%Gsu!rT~LiHWL~>M z)Vj4;)zRgGrd#I@SoyX`iKSh(l2i10t#(kSVYhAHp#;OKk%~5_)@v4-o|+zVro1&Y zJtT*S*?wp{ksJ8`UmpF!tn8@*FWZw7iZ(jWOxpH%p?7mW#FOjMFtWW4sbI^j>Vo#D zK(lqjz>=@(a>RTC%PlVDFlHZ@=Ho$t@7sbd+Ku7q0VHJx$D%QY|?ew4muw7qo=Oh zoz8O8j_igV(y7iyoOOMdAI3O{Ie5Y|$hvTU09}=><|i5xbYJHyk^_~(3NbQ4L@kR z5q&kz@)l7iigS30)QhUA#gY+Ri}NhFwz7&y3D`!QaRSeCqd4ufgOIW;#Do|U%GN+>HCHi{~-+<$7@ zZiDe>k`+1Q_alky4)jojB-A+fwih(l+Vnho9@qHI3>9QMk{}2_LGfpJ`9c18H}^;C Xh)~!$4dRjC#jG&alLitSL+$?nm?uVj literal 0 HcmV?d00001 diff --git a/app/__pycache__/main.cpython-313.pyc b/app/__pycache__/main.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..34fb0944d13a33f3a02df980b9d91208e61d21e9 GIT binary patch literal 6988 zcmdrQTW}NC_3lIOmn~sSV1pM(0D&KbI4N%<+u~v4N4!>IY7vi+RoQ^#^w4DX6sJ89m%3BARZur>hJ z9s{;K@*cpdJ3#aG(MG`WuEO!J@g`px2i|4xWcdxN@B(Y#H9UdWxC(FM8hHNbCY?uD z)B!vOR^c?Q@m(9E%~7WbKD6TNHm$w}BQIHx8QrHC$3upy;q=kA9Mqdj><%|$-OJMO`L263~?+U4OW>S!p zHP>UAOms3PO6{3TxDM#(oed^3(Zeg~AFShNr!eR!%Gx?&g>#V~{so@lgx$D!XcLp^ zgpkr4@IA_uJQ0T{jZMUqSiHnNV3^^jf7w4_QtQiTBvBD+guqlao{FFmwrUX`VU=n!WdA-7A4O$x2txmYSD#f2dEdgyc{%yYZCTDh*^ z0;ADlLSqSxW-(w)*bOh!EW&$2d`b~;GkNk5Q4?dSi6WF{OM%$q^hGfdWHFH;jg6-z zNi)Z%u$;!4y~x6Zn2^bda?tPw0_TqF0zXj%z%Rfe5ivOe9f*R>^Yg&$=yH9-$IUmJ zmz~F!UG+E4%$%8HmjbQ1K&$F(TQM~06e*(hZ2%va7bhTybkFfLX zGSx{L_c=lgR#h`BIFX{#J%i^OH+&TMBA(&d1GGSm6Q$8xgf?u`bIOcq@bPv&d^+4S z%I)Tkj|>fzrRI&3;gK*WCb%P9*TWA%b3b?_iUF>-SR zR&NzISGsxQzQGO=yHUDj0|z{sDS|3wrgZ0J2!n>iM3_}FQ+_i<9tD|+Fuaw#yf)Wu zup_Z+@L?M}ROA4EKgc!(~zD!=r|EB+|}RivHu?N}H+> zFI@1wGPkR2HCj34QU+{nOy^~_i6vH6vlJytx1@Lzu(4p$%mr{=L3D$wYYrv>+=8i~ zDa5Mk>VnA~X%0!aXqaD_+$ZQx(Wc@~0!q3T_{j^*iy*wLRwD=#mo9-}b z=RnRqs9Faf6iw{2=+koJ#<}24XQ|k6|7~JycCgm|t#^_Od+zPYc?KUfl=PAq=ii2ocDFH~x7oW7 zS+Xr_DCWgtKvY{ZfB%1{nFqZ!tch)1It?~xO`W!{OQ#*6)6RA2v`e=wEbo49o%R%T z8t(;3z%P+*P5t$rlH{pmQjCibZgNTRLo(h+FkXgEvl=2&Ry9m4CH@g~)jD&CY>=*l zZ2k$lA6#b~?&(W2N9THf-T$lpMaRyfhdQ0}4yew7XV<7r^P{)NZ;h+YmuhL$#?>0t z-PB`ezOdVS0+y`3LpNTKLddJanvz@l57<<2Yv>1ZYb3m0!>!c{lPg>uane<-k)u%@t#hi+3tBOC?i?^o=uV4(FhR#oXx2+iKXLXx#MGM_{WR&<4g64Tzz7(J~?gv+T$-8Kt2k_-0S#99!l$+-|znwCHGEZNR@Z0-ynPcywuaJU2Y9o{cS?eJ6MJ z9d!tQX_ZSMQWZoxcQ&mKWxlk&QyDVVFS)knTwCXle$x5bp-&H~&Mw2u+gv2l(&H)s zYRS1;<~M)Be&+bpp*jy6K=qr{roF1?rANWlj-dBYcsKKh-S+T)OLqGjp_P6W#n`sC zhv?M9t0&7uPcNoa*kEEFt6GI+|K7wGtVYQ)m&rU^qU1xbvohU*WZH*LLXksa$4mYu z!W!xHFg*YzDhJ{KP6GHR3o)rklRZn7sxGE^p>PQ-1 z$qYUPh~w~+KY;G}a`9!4@5cKx?=L$ekQYo3K>4Q5{>Y3RzL{Oucjqn0=K9Wxw(VH- z?D)-wxzLB~>}$WY+}e=y?08fUqC6A$@4xC$-A?`C7f{V1C5G#vK(~##zrCTGHQ(P) z!~ApBM0%UO+i%HQI!KLBj6)>?rzJ&@5>O7cB}6R5l{CJjnG4%c@6oGUR7V_WmKqT> z@_V8-%2S4rya0vC!K}}q-XTf};l1_?%47hP<9wnay~%C_rRgF#NSB%#6<%ZQk;Qn*!LajCt)ev(v{F;Oh7-qoTmJwTXG*_K`whKY8p4y<-JZ^tRV z1zge`eM$d;p7QquiW%^!C*1;8?s;!kDr%6Z4sYMrzJ0&nyf?b+bXp95e?`Bmj}Bx1 zkg@prbLH|+RNk_f#m*kvb-)>r-q>sGy5L&f-Sc)m@CIy$-C?nJoyC6QJap^#!MEN3 zz2TBKu-*v0%_VPWy)E=^EO{gAZKJodj>p*5)I$68X#2But6 zN9M`XZ;~G$Csip&_fk+VWqvP}FJ)Gg6PfF^eo>YQb}6&!et7~RR9g~jgI9-VKmY9w;EP4hYPKI>cs<)mfK0o@^a{?yIp@Hcr2u#BWn-)&ycgV^w1rhO4mD&08$15ZuBtK!WO3bYgf$?akNw zb>9?O-S_{T3~fYxGIU0ZZ3yi)L_dMZt0M_hYRhzwpqOw`ZwObZ>F8p~gaIba5k@N! zenklu;o5_`Ln$OEts){2=GM-8Z|A(f{odPNf=pSfA^(7p^+#ndfzKe@vGnbL5)A=q zafp{fguA_AX%Hh?(CIct;2H|kT4KV}C#AFmm?m9GxU`e9<^pce8=uW1)dvz(uA&Tt zY3K23|MajP%J$~z{(1M`$)xOqVar72Nh)%@62L`|3GmA*AA^Sm(EirjYzN-XjAhh* z@4r2m=uv%7eG7C|;(r9Ic2RtA9mly~w=UR?3wCqnHJsaL&t?oCv&eP&Z`6$8W7cw< d-Wlrnm<6uWdvikm-mK+2{j=dmh7UWLp8$?#SXBT3 literal 0 HcmV?d00001 diff --git a/app/database.py b/app/database.py new file mode 100644 index 00000000..a83bcdd7 --- /dev/null +++ b/app/database.py @@ -0,0 +1,32 @@ +import sqlite3 +from contextlib import contextmanager + +DATABASE = "banking.db" + +def init_db(): + with get_db() as conn: + conn.executescript(""" + CREATE TABLE IF NOT EXISTS accounts ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + name TEXT NOT NULL, + balance REAL DEFAULT 0.0 + ); + CREATE TABLE IF NOT EXISTS transactions ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + account_id INTEGER NOT NULL, + type TEXT NOT NULL, + amount REAL NOT NULL, + timestamp DATETIME DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY (account_id) REFERENCES accounts(id) + ); + """) + +@contextmanager +def get_db(): + conn = sqlite3.connect(DATABASE) + conn.row_factory = sqlite3.Row + try: + yield conn + conn.commit() + finally: + conn.close() \ No newline at end of file diff --git a/app/main.py b/app/main.py new file mode 100644 index 00000000..f5daba73 --- /dev/null +++ b/app/main.py @@ -0,0 +1,108 @@ +from fastapi import FastAPI, HTTPException +from fastapi.staticfiles import StaticFiles +from fastapi.responses import FileResponse +from app.models import AccountCreate, Account, DepositWithdraw, Transfer, Transaction +from app.database import init_db, get_db + +app = FastAPI(title="Banking API") + +@app.on_event("startup") +def startup(): + init_db() + +@app.post("/accounts", response_model=Account, status_code=201) +def create_account(data: AccountCreate): + with get_db() as conn: + cursor = conn.execute( + "INSERT INTO accounts (name, balance) VALUES (?, ?)", + (data.name, data.initial_balance) + ) + account_id = cursor.lastrowid + return {"id": account_id, "name": data.name, "balance": data.initial_balance} + +@app.get("/accounts/{account_id}", response_model=Account) +def get_account(account_id: int): + with get_db() as conn: + row = conn.execute("SELECT * FROM accounts WHERE id = ?", (account_id,)).fetchone() + if not row: + raise HTTPException(status_code=404, detail="Account not found") + return dict(row) + +@app.post("/accounts/{account_id}/deposit", response_model=Account) +def deposit(account_id: int, data: DepositWithdraw): + if data.amount <= 0: + raise HTTPException(status_code=400, detail="Amount must be positive") + with get_db() as conn: + row = conn.execute("SELECT * FROM accounts WHERE id = ?", (account_id,)).fetchone() + if not row: + raise HTTPException(status_code=404, detail="Account not found") + new_balance = row["balance"] + data.amount + conn.execute("UPDATE accounts SET balance = ? WHERE id = ?", (new_balance, account_id)) + conn.execute( + "INSERT INTO transactions (account_id, type, amount) VALUES (?, ?, ?)", + (account_id, "deposit", data.amount) + ) + return {"id": account_id, "name": row["name"], "balance": new_balance} + +@app.post("/accounts/{account_id}/withdraw", response_model=Account) +def withdraw(account_id: int, data: DepositWithdraw): + if data.amount <= 0: + raise HTTPException(status_code=400, detail="Amount must be positive") + with get_db() as conn: + row = conn.execute("SELECT * FROM accounts WHERE id = ?", (account_id,)).fetchone() + if not row: + raise HTTPException(status_code=404, detail="Account not found") + if row["balance"] < data.amount: + raise HTTPException(status_code=400, detail="Insufficient funds") + new_balance = row["balance"] - data.amount + conn.execute("UPDATE accounts SET balance = ? WHERE id = ?", (new_balance, account_id)) + conn.execute( + "INSERT INTO transactions (account_id, type, amount) VALUES (?, ?, ?)", + (account_id, "withdraw", data.amount) + ) + return {"id": account_id, "name": row["name"], "balance": new_balance} + +@app.post("/transfers", response_model=dict) +def transfer(data: Transfer): + if data.amount <= 0: + raise HTTPException(status_code=400, detail="Amount must be positive") + with get_db() as conn: + from_acc = conn.execute("SELECT * FROM accounts WHERE id = ?", (data.from_account_id,)).fetchone() + to_acc = conn.execute("SELECT * FROM accounts WHERE id = ?", (data.to_account_id,)).fetchone() + if not from_acc: + raise HTTPException(status_code=404, detail="Source account not found") + if not to_acc: + raise HTTPException(status_code=404, detail="Destination account not found") + if from_acc["balance"] < data.amount: + raise HTTPException(status_code=400, detail="Insufficient funds") + + conn.execute("UPDATE accounts SET balance = balance - ? WHERE id = ?", (data.amount, data.from_account_id)) + conn.execute("UPDATE accounts SET balance = balance + ? WHERE id = ?", (data.amount, data.to_account_id)) + conn.execute( + "INSERT INTO transactions (account_id, type, amount) VALUES (?, ?, ?)", + (data.from_account_id, "transfer_out", data.amount) + ) + conn.execute( + "INSERT INTO transactions (account_id, type, amount) VALUES (?, ?, ?)", + (data.to_account_id, "transfer_in", data.amount) + ) + return {"message": "Transfer successful"} + +@app.get("/accounts/{account_id}/transactions", response_model=list[Transaction]) +def get_transactions(account_id: int): + with get_db() as conn: + row = conn.execute("SELECT * FROM accounts WHERE id = ?", (account_id,)).fetchone() + if not row: + raise HTTPException(status_code=404, detail="Account not found") + rows = conn.execute( + "SELECT * FROM transactions WHERE account_id = ? ORDER BY timestamp DESC", + (account_id,) + ).fetchall() + return [dict(r) for r in rows] + +# Serve frontend +app.mount("/static", StaticFiles(directory="frontend"), name="static") + +@app.get("/") +def serve_frontend(): + return FileResponse("frontend/index.html") \ No newline at end of file diff --git a/app/models.py b/app/models.py new file mode 100644 index 00000000..b83bef58 --- /dev/null +++ b/app/models.py @@ -0,0 +1,27 @@ +from pydantic import BaseModel +from typing import Optional +from datetime import datetime + +class AccountCreate(BaseModel): + name: str + initial_balance: float = 0.0 + +class Account(BaseModel): + id: int + name: str + balance: float + +class DepositWithdraw(BaseModel): + amount: float + +class Transfer(BaseModel): + from_account_id: int + to_account_id: int + amount: float + +class Transaction(BaseModel): + id: int + account_id: int + type: str + amount: float + timestamp: str \ No newline at end of file diff --git a/banking.db b/banking.db new file mode 100644 index 0000000000000000000000000000000000000000..565f933a2a10e7b7d8ffe0362900d7721c05bd4f GIT binary patch literal 16384 zcmeI&ON-M$00;1yBy8EmHLD0cNO43eE&Cw0uF9Uww$mk;rZt_2JxIy65dwW}lV#DH z3ts#{eun)H;=zk2PksU?t-(HUy?AitKQx{9Wac-g3DbIRhjGAOk0t{@W@WNPC?!uB zBZOq+qsb=?icHMsXSjpvC)dBq2mt~RfB*y_009U<00Izz!2d09 z{#MOo^LhIIaqJ)WgLvW(BflGmqhU0k)+#PH1!uxMv^krvVTBF06v7^}9Knyc%bKoL zH{CXS#@o#FM8k6AKy~hjot0jGw>vr=#+|v|PD3!qv+dQ!@tbkL1b->(f7`|%B;zqT zar=^1aX1K~*dL5pRW49ibO5)~9bbWR*|HkYI4ucl+BuTEoSVU)D=uWey6jX&+mvETRQ@@CtTN*jB|qD*o&*YuA>|E7P^ zFJuA%0uX=z1Rwwb2tWV=5P$##AOL}XL|{wP$USP0&e-AXl)rI9)kvOJgYhT|V>{>vnWRxE58l_!hpBd$Y^8Uethd%&kG~=xR literal 0 HcmV?d00001 diff --git a/frontend/index.html b/frontend/index.html new file mode 100644 index 00000000..70f166e8 --- /dev/null +++ b/frontend/index.html @@ -0,0 +1,165 @@ + + + + + + Banking App + + + +

🏦 Banking App

+ +
+

Create Account

+ + + +
+
+ +
+

View Account

+ + +
+
+ +
+

Deposit

+ + + +
+
+ +
+

Withdraw

+ + + +
+
+ +
+

Transfer

+ + + + +
+
+ +
+

Transaction History

+ + +
+
+ + + + \ No newline at end of file diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 00000000..7893c98a --- /dev/null +++ b/requirements.txt @@ -0,0 +1,7 @@ +fastapi +uvicorn +pydantic +pytest +httpx +playwright +pytest-playwright \ No newline at end of file diff --git a/tests/__pycache__/test_api.cpython-313-pytest-9.0.2.pyc b/tests/__pycache__/test_api.cpython-313-pytest-9.0.2.pyc new file mode 100644 index 0000000000000000000000000000000000000000..59dfd0bc8a7a7e1d1a4bf2a043dadc82c515216f GIT binary patch literal 25989 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 literal 0 HcmV?d00001 diff --git a/tests/test_api.py b/tests/test_api.py new file mode 100644 index 00000000..05088ca0 --- /dev/null +++ b/tests/test_api.py @@ -0,0 +1,149 @@ +import pytest +from fastapi.testclient import TestClient +import sys +import os +sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) + +from app.main import app + + +@pytest.fixture(autouse=True) +def clean_db(): + """Reset database before each test""" + from app.database import init_db, DATABASE + if os.path.exists(DATABASE): + os.remove(DATABASE) + init_db() # Initialize the database + yield + if os.path.exists(DATABASE): + os.remove(DATABASE) + +@pytest.fixture +def client(): + return TestClient(app) + +class TestAccountCreation: + def test_create_account_success(self, client): + response = client.post("/accounts", json={"name": "John Doe", "initial_balance": 100.0}) + assert response.status_code == 201 + data = response.json() + assert data["name"] == "John Doe" + assert data["balance"] == 100.0 + assert "id" in data + + def test_create_account_zero_balance(self, client): + response = client.post("/accounts", json={"name": "Jane Doe"}) + assert response.status_code == 201 + assert response.json()["balance"] == 0.0 + +class TestGetAccount: + def test_get_account_success(self, client): + create_res = client.post("/accounts", json={"name": "Test User", "initial_balance": 50.0}) + account_id = create_res.json()["id"] + + response = client.get(f"/accounts/{account_id}") + assert response.status_code == 200 + assert response.json()["name"] == "Test User" + assert response.json()["balance"] == 50.0 + + def test_get_account_not_found(self, client): + response = client.get("/accounts/9999") + assert response.status_code == 404 + assert response.json()["detail"] == "Account not found" + +class TestDeposit: + def test_deposit_success(self, client): + create_res = client.post("/accounts", json={"name": "Depositor", "initial_balance": 100.0}) + account_id = create_res.json()["id"] + + response = client.post(f"/accounts/{account_id}/deposit", json={"amount": 50.0}) + assert response.status_code == 200 + assert response.json()["balance"] == 150.0 + + def test_deposit_invalid_amount(self, client): + create_res = client.post("/accounts", json={"name": "Test", "initial_balance": 100.0}) + account_id = create_res.json()["id"] + + response = client.post(f"/accounts/{account_id}/deposit", json={"amount": -10.0}) + assert response.status_code == 400 + assert response.json()["detail"] == "Amount must be positive" + + def test_deposit_account_not_found(self, client): + response = client.post("/accounts/9999/deposit", json={"amount": 50.0}) + assert response.status_code == 404 + +class TestWithdraw: + def test_withdraw_success(self, client): + create_res = client.post("/accounts", json={"name": "Withdrawer", "initial_balance": 100.0}) + account_id = create_res.json()["id"] + + response = client.post(f"/accounts/{account_id}/withdraw", json={"amount": 30.0}) + assert response.status_code == 200 + assert response.json()["balance"] == 70.0 + + def test_withdraw_insufficient_funds(self, client): + create_res = client.post("/accounts", json={"name": "Broke", "initial_balance": 20.0}) + account_id = create_res.json()["id"] + + response = client.post(f"/accounts/{account_id}/withdraw", json={"amount": 50.0}) + assert response.status_code == 400 + assert response.json()["detail"] == "Insufficient funds" + + def test_withdraw_invalid_amount(self, client): + create_res = client.post("/accounts", json={"name": "Test", "initial_balance": 100.0}) + account_id = create_res.json()["id"] + + response = client.post(f"/accounts/{account_id}/withdraw", json={"amount": 0}) + assert response.status_code == 400 + +class TestTransfer: + def test_transfer_success(self, client): + acc1 = client.post("/accounts", json={"name": "Sender", "initial_balance": 100.0}).json() + acc2 = client.post("/accounts", json={"name": "Receiver", "initial_balance": 50.0}).json() + + response = client.post("/transfers", json={ + "from_account_id": acc1["id"], + "to_account_id": acc2["id"], + "amount": 30.0 + }) + assert response.status_code == 200 + assert response.json()["message"] == "Transfer successful" + + assert client.get(f"/accounts/{acc1['id']}").json()["balance"] == 70.0 + assert client.get(f"/accounts/{acc2['id']}").json()["balance"] == 80.0 + + def test_transfer_insufficient_funds(self, client): + acc1 = client.post("/accounts", json={"name": "Sender", "initial_balance": 10.0}).json() + acc2 = client.post("/accounts", json={"name": "Receiver", "initial_balance": 50.0}).json() + + response = client.post("/transfers", json={ + "from_account_id": acc1["id"], + "to_account_id": acc2["id"], + "amount": 100.0 + }) + assert response.status_code == 400 + assert response.json()["detail"] == "Insufficient funds" + + def test_transfer_account_not_found(self, client): + acc1 = client.post("/accounts", json={"name": "Sender", "initial_balance": 100.0}).json() + + response = client.post("/transfers", json={ + "from_account_id": acc1["id"], + "to_account_id": 9999, + "amount": 30.0 + }) + assert response.status_code == 404 + +class TestTransactions: + def test_get_transactions(self, client): + acc = client.post("/accounts", json={"name": "Active User", "initial_balance": 100.0}).json() + client.post(f"/accounts/{acc['id']}/deposit", json={"amount": 50.0}) + client.post(f"/accounts/{acc['id']}/withdraw", json={"amount": 20.0}) + + response = client.get(f"/accounts/{acc['id']}/transactions") + assert response.status_code == 200 + transactions = response.json() + assert len(transactions) == 2 + types = [t["type"] for t in transactions] + assert "deposit" in types + assert "withdraw" in types \ No newline at end of file diff --git a/tests/test_ui.py b/tests/test_ui.py new file mode 100644 index 00000000..574aa188 --- /dev/null +++ b/tests/test_ui.py @@ -0,0 +1,66 @@ +import pytest +from playwright.sync_api import Page, expect + + + + + +BASE_URL = "http://localhost:8000" + +@pytest.fixture(scope="session") +def browser_context_args(): + return {"viewport": {"width": 1280, "height": 720}} + +class TestBankingUI: + def test_create_account(self, page: Page): + page.goto(BASE_URL) + page.fill("#accountName", "UI Test User") + page.fill("#initialBalance", "500") + page.click("button:has-text('Create')") + expect(page.locator("#createResult")).to_contain_text("Account created") + + def test_view_account(self, page: Page): + page.goto(BASE_URL) + # Create account first + page.fill("#accountName", "View Test") + page.fill("#initialBalance", "200") + page.click("button:has-text('Create')") + page.wait_for_selector("#createResult:has-text('Account created')") + + # View account + page.fill("#viewAccountId", "1") + page.click("button:has-text('View')") + expect(page.locator("#viewResult")).to_contain_text("Balance") + + def test_deposit(self, page: Page): + page.goto(BASE_URL) + # Create account + page.fill("#accountName", "Deposit Test") + page.fill("#initialBalance", "100") + page.click("button:has-text('Create')") + page.wait_for_selector("#createResult:has-text('Account created')") + + # Deposit + page.fill("#depositAccountId", "1") + page.fill("#depositAmount", "50") + page.click("button:has-text('Deposit')") + expect(page.locator("#depositResult")).to_contain_text("Deposited") + + def test_withdraw_insufficient_funds(self, page: Page): + page.goto(BASE_URL) + # Create account with low balance + page.fill("#accountName", "Low Balance") + page.fill("#initialBalance", "10") + page.click("button:has-text('Create')") + page.wait_for_selector("#createResult:has-text('Account created')") + + # Get the created account ID from the result + result_text = page.locator("#createResult").text_content() + # Extract ID from "Account created! ID: X, Name: ..." + account_id = result_text.split("ID: ")[1].split(",")[0] + + # Try to withdraw more than balance + page.fill("#withdrawAccountId", account_id) + page.fill("#withdrawAmount", "100") + page.click("button:has-text('Withdraw')") + expect(page.locator("#withdrawResult")).to_contain_text("Insufficient funds") \ No newline at end of file