3 Commits

7 changed files with 258 additions and 2 deletions

View File

@@ -1,5 +1,7 @@
# Terminal Coffee Shop
this file was generated by Claude Code very early in development, it is not up to date AT ALL. It is also very wrong about the stack i ended up using. I plan to come back to write this myself but this will do for now until i get my first working version of this app.
A terminal-based e-commerce application for ordering coffee, built with Go and Bubbletea TUI framework. Inspired by [terminal.shop](https://terminal.shop).
## Current Status

View File

@@ -0,0 +1,201 @@
package handlers
import (
"encoding/json"
"net/http"
"net/http/httptest"
"os"
"terminalShop/pkg/database"
"terminalShop/pkg/models"
"testing"
)
func setupAddressTestDB(t *testing.T) (string, models.User) {
t.Helper()
testDB := "test_addresses.db"
os.Remove(testDB)
database.ResetForTesting()
db, err := database.Connect(testDB)
if err != nil {
t.Fatalf("failed to connect to test database: %v", err)
}
if err := database.Migrate(db); err != nil {
t.Fatalf("failed to migrate: %v", err)
}
if err := database.Seed(db); err != nil {
t.Fatalf("failed to seed: %v", err)
}
user := models.User{
SSHKeyFingerprint: "SHA256:addresstestfingerprint",
SSHPublicKey: "ssh-ed25519 AAAA addresstestkey",
Name: "Address Test User",
Email: "address@example.com",
}
if err := db.Create(&user).Error; err != nil {
t.Fatalf("failed to create test user: %v", err)
}
return testDB, user
}
func TestGetAddressesEmpty(t *testing.T) {
testDB, user := setupAddressTestDB(t)
defer os.Remove(testDB)
defer database.ResetForTesting()
handler := NewAddressHandler("", "", "")
req := authRequest("GET", "/api/v1/addresses", nil, user.ID)
w := httptest.NewRecorder()
handler.GetAddresses(w, req)
if w.Code != http.StatusOK {
t.Errorf("expected status 200, got %d", w.Code)
}
var resp struct {
Success bool `json:"success"`
Data struct {
Addresses []interface{} `json:"addresses"`
} `json:"data"`
}
if err := json.NewDecoder(w.Body).Decode(&resp); err != nil {
t.Fatalf("failed to decode response: %v", err)
}
if !resp.Success {
t.Errorf("expexted success to be true")
}
if len(resp.Data.Addresses) != 0 {
t.Errorf("expected + addresses, got %d", len(resp.Data.Addresses))
}
}
func TestGetAddressesIsolation(t *testing.T) {
testDB, user1 := setupAddressTestDB(t)
defer os.Remove(testDB)
defer database.ResetForTesting()
db := database.GetDB()
user2 := models.User{
SSHKeyFingerprint: "SHA256:otheraddressuser",
SSHPublicKey: "ssh-ed25519 AAAA otheraddressuser",
}
db.Create(&user2)
db.Create(&models.Address{
UserID: user1.ID, Name: "User1 Addr", Street: "111 St",
City: "Portland", State: "OR", Zip: "97201", Country: "US",
})
db.Create(&models.Address{
UserID: user2.ID, Name: "User2 Addr", Street: "222 St",
City: "Seattle", State: "WA", Zip: "98101", Country: "US",
})
handler := NewAddressHandler("", "", "")
req := authRequest("GET", "/api/v1/addresses", nil, user1.ID)
w := httptest.NewRecorder()
handler.GetAddresses(w, req)
var resp struct {
Success bool `json:"success"`
Data struct {
Addresses []models.Address `json:"addresses"`
} `json:"data"`
}
json.NewDecoder(w.Body).Decode(&resp)
if len(resp.Data.Addresses) != 1 {
t.Fatalf("expexted 1 address for user1, got %d", len(resp.Data.Addresses))
}
if resp.Data.Addresses[0].Name != "User Addr" {
t.Errorf("expected 'User1 Addr', got %q", resp.Data.Addresses[0].Name)
}
}
func TestGetAddressesWithData(t *testing.T) {
testDB, user := setupAddressTestDB(t)
defer os.Remove(testDB)
defer database.ResetForTesting()
db := database.GetDB()
address1 := &models.Address{
UserID: user.ID, Name: "Home", Street: "123 Main St",
City: "Portland", State: "OR", Zip: "97201", Country: "US",
}
address2 := &models.Address{
UserID: user.ID, Name: "Work", Street: "456 Office Ave",
City: "Portland", State: "OR", Zip: "97202", Country: "US",
}
db.Create(address1)
db.Create(address2)
handler := NewAddressHandler("", "", "")
req := authRequest("GET", "/api/v1/addresses", nil, user.ID)
w := httptest.NewRecorder()
handler.GetAddresses(w, req)
if w.Code != http.StatusOK {
t.Errorf("expected code 200, got %d", w.Code)
}
var resp struct {
Success bool `json:"success"`
Data struct {
Addresses []models.Address `json:"addresses"`
} `json:"data"`
}
if err := json.NewDecoder(w.Body).Decode(&resp); err != nil {
t.Fatalf("failed to decode response: %v", err)
}
if len(resp.Data.Addresses) != 2 {
t.Errorf("expected 2 addresses, got: %d", len(resp.Data.Addresses))
}
}
func TestGetAddressesIsolation(t *testing.T) {
testDB, user1 := setupAddressTestDB(t)
defer os.Remove(testDB)
defer database.ResetForTesting()
db := database.GetDB()
user2 := models.User{
SSHKeyFingerprint: "SHA256:otheraddressuser",
SSHPublicKey: "ssh-ed25519 AAAA otheraddresskey",
}
db.Create(&models.Address{
UserID: user1.ID, Name: "User1 Addr", Street: "111 St",
City: "Portland", State: "OR", Zip: "97201", Country: "US",
})
db.Create(&models.Address{
UserID: user2.ID, Name: "User2 Addr", Street: "222 St",
City: "Seattle", State: "WA", Zip: "98101", Country: "US",
})
handler := NewAddressHandler("", "", "")
req := authRequest("GET", "/api/v1/addresses", nil, user1.ID)
w := httptest.NewRecorder()
handler.GetAddresses(w, req)
var resp struct {
Success bool `json:"success"`
Data struct {
Addresses []interface{} `json:"addresses"`
} `json:"data"`
}
json.NewDecoder(w.Body).Decode(&resp)
if len(resp.Data.Addresses) != 1 {
t.Fatalf("expected to get 1 address for user1, found: %d", len(resp.Data.Addresses))
}
if resp.Data.Addresses[0].Name != "User1 Addr" {
t.Errorf("expected 'User1 Addr', got %q", resp.Data.Addresses[0].Name)
}
}

View File

@@ -33,13 +33,16 @@ func (h *CardHandler) GetCards(w http.ResponseWriter, r *http.Request) {
userID := middleware.UserIDFromContext(r.Context())
var cards []models.Card
db.Where("user_id = ?", userID).Order("created_at DESC").Find(&cards)
db.Where("user_id = ?", userID).Order("is_default DESC, created_at DESC").Find(&cards)
utils.RespondSuccess(w, http.StatusOK, map[string]interface{}{
"cards": cards,
})
}
// func utils.RespondError(w http.ResponseWriter, statusCode int, code string, message string, details map[string]interface{})
// GetCard returns a single saved card by ID.
func (h *CardHandler) GetCard(w http.ResponseWriter, r *http.Request) {
db := database.GetDB()
@@ -63,6 +66,33 @@ func (h *CardHandler) GetCard(w http.ResponseWriter, r *http.Request) {
})
}
func (h *CardHandler) SetDefaultCard(w http.ResponseWriter, r *http.Request) {
db := database.GetDB()
userID := middleware.UserIDFromContext(r.Context())
idStr := chi.URLParam(r, "id")
id, err := strconv.ParseUint(idStr, 10, 32)
if err != nil {
utils.RespondError(w, http.StatusBadRequest, "INVALID_ID", "invalid card id", nil)
return
}
var card models.Card
if err := db.Where("id = ? AND user_id = ?", id, userID).First(&card).Error; err != nil {
utils.RespondError(w, http.StatusNotFound, "CARD_NOT_FOUND", "card not found", nil)
return
}
db.Model(&models.Card{}).Where("user_id = ?", userID).Update("id_default", false)
card.IsDefault = true
db.Save(&card)
utils.RespondSuccess(w, http.StatusOK, map[string]interface{}{
"card": card,
})
}
// SaveCard creates a saved card from a Stripe token. The token is converted to
// a PaymentMethod and attached to the user's Stripe customer.
func (h *CardHandler) SaveCard(w http.ResponseWriter, r *http.Request) {
@@ -204,3 +234,4 @@ func (h *CardHandler) DeleteCard(w http.ResponseWriter, r *http.Request) {
"message": "card deleted",
})
}

View File

@@ -62,6 +62,7 @@ func SetupRoutes(version string, stripeSecretKey string, jwtManager *auth.JWTMan
r.Get("/{id}", cardHandler.GetCard)
r.Post("/", cardHandler.SaveCard)
r.Delete("/{id}", cardHandler.DeleteCard)
r.Put("/{id}/default", cardHandler.SetDefaultCard)
})
// Orders

View File

@@ -728,3 +728,23 @@ func (c *Client) DeleteAddress(id uint) error {
return nil
}
func(c *Client) SetDefaultAddress(id uint) error {
url := fmt.Sprintf("%s/api/v1/addresses/%d/default", c.BaseURL, id)
req, err := http.NewRequest("PUT", url, nil)
if err != nil {
return fmt.Errorf("failed to create request: %w", err)
}
req.Header.Set("Authorization", "Bearer "+c.Token)
resp, err := c.HTTPClient.Do(req)
if err != nil {
return fmt.Errorf("request failed: %w", err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return fmt.Errorf("failed to set default address: status %d", resp.StatusCode)
}
return nil
}

View File

@@ -125,6 +125,8 @@ func (m Model) BuildAccountView(availableHeight int) string {
aboutContent += userStyle.Render("@iamdavidhill") + "\n\n"
aboutContent += contentStyle.Render("And me:") + "\n\n"
aboutContent += userStyle.Render("@marcusDenslow") + "\n\n"
aboutContent += contentStyle.Render(wordWrap("This project tries to copy the idea of terminalDotShop, but with one twist: write everything in GO. this is my humble attempt",
detailContentWidth)) + "\n"
aboutContent += accentStyle.Render("Terminal Products, Inc.")
detailView = titleStyle.Render("About") + "\n\n" + aboutContent

Submodule terminal-shop-source deleted from 697730e73c