Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 11 additions & 2 deletions handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -123,7 +123,12 @@ func handleCreateList(store *Store) http.HandlerFunc {
}
title = truncateRunes(title, maxTitleLen)
desc := truncateRunes(strings.TrimSpace(r.FormValue("description")), maxDescriptionLen)
l := store.CreateList(title, desc)
l, err := store.CreateList(title, desc)
if err != nil {
slog.Warn("リスト作成拒否", "error", err)
http.Error(w, err.Error(), http.StatusConflict)
return
}
http.Redirect(w, r, "/lists/"+l.ShareToken, http.StatusSeeOther)
}
}
Expand Down Expand Up @@ -170,7 +175,11 @@ func handleAddItem(store *Store) http.HandlerFunc {
name = truncateRunes(name, maxItemNameLen)
assignee := truncateRunes(strings.TrimSpace(r.FormValue("assignee")), maxAssigneeLen)
required := r.FormValue("required") == "on"
store.AddItem(token, name, assignee, required)
if _, err := store.AddItem(token, name, assignee, required); err != nil {
slog.Warn("アイテム追加拒否", "token", token, "error", err)
http.Error(w, err.Error(), http.StatusConflict)
return
}
http.Redirect(w, r, "/lists/"+token, http.StatusSeeOther)
}
}
Expand Down
31 changes: 15 additions & 16 deletions handler_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import (
)

func setupTestServer() (*http.ServeMux, *Store) {
store := NewStore()
store := NewStore(0, 0)
mux := http.NewServeMux()
registerRoutes(mux, store)
return mux, store
Expand Down Expand Up @@ -52,7 +52,7 @@ func TestHealthEndpoint(t *testing.T) {

func TestHealthEndpointWithData(t *testing.T) {
mux, store := setupTestServer()
l := store.CreateList("テスト", "")
l, _ := store.CreateList("テスト", "")
store.AddItem(l.ShareToken, "テント", "太郎", true)
store.AddItem(l.ShareToken, "寝袋", "花子", false)

Expand Down Expand Up @@ -122,7 +122,7 @@ func TestCreateListAndView(t *testing.T) {

func TestAddItemAndToggle(t *testing.T) {
mux, store := setupTestServer()
l := store.CreateList("テスト", "")
l, _ := store.CreateList("テスト", "")
token := l.ShareToken

// Add item
Expand Down Expand Up @@ -167,7 +167,7 @@ func TestAddItemAndToggle(t *testing.T) {

func TestDeleteItem(t *testing.T) {
mux, store := setupTestServer()
l := store.CreateList("テスト", "")
l, _ := store.CreateList("テスト", "")
store.AddItem(l.ShareToken, "寝袋", "", true)

item := store.GetList(l.ShareToken).Items[0]
Expand All @@ -182,7 +182,7 @@ func TestDeleteItem(t *testing.T) {

func TestUpdateAssignee(t *testing.T) {
mux, store := setupTestServer()
l := store.CreateList("テスト", "")
l, _ := store.CreateList("テスト", "")
store.AddItem(l.ShareToken, "クーラーボックス", "太郎", true)

item := store.GetList(l.ShareToken).Items[0]
Expand Down Expand Up @@ -236,7 +236,7 @@ func TestCreateListWhitespaceTitle(t *testing.T) {

func TestDeleteList(t *testing.T) {
mux, store := setupTestServer()
l := store.CreateList("削除テスト", "")
l, _ := store.CreateList("削除テスト", "")
token := l.ShareToken

req := httptest.NewRequest("POST", "/lists/"+token+"/delete", nil)
Expand Down Expand Up @@ -267,7 +267,7 @@ func TestDeleteListNotFound(t *testing.T) {

func TestAddItemEmptyName(t *testing.T) {
mux, store := setupTestServer()
l := store.CreateList("テスト", "")
l, _ := store.CreateList("テスト", "")
token := l.ShareToken

form := url.Values{"name": {""}, "assignee": {"太郎"}}
Expand All @@ -287,7 +287,7 @@ func TestAddItemEmptyName(t *testing.T) {

func TestAddItemWhitespaceName(t *testing.T) {
mux, store := setupTestServer()
l := store.CreateList("テスト", "")
l, _ := store.CreateList("テスト", "")
token := l.ShareToken

form := url.Values{"name": {" "}}
Expand All @@ -306,7 +306,7 @@ func TestAddItemWhitespaceName(t *testing.T) {

func TestTogglePreparedInvalidItem(t *testing.T) {
mux, store := setupTestServer()
l := store.CreateList("テスト", "")
l, _ := store.CreateList("テスト", "")
token := l.ShareToken

req := httptest.NewRequest("POST", "/lists/"+token+"/items/nonexistent-id/toggle-prepared", nil)
Expand All @@ -321,7 +321,7 @@ func TestTogglePreparedInvalidItem(t *testing.T) {

func TestToggleRequiredInvalidItem(t *testing.T) {
mux, store := setupTestServer()
l := store.CreateList("テスト", "")
l, _ := store.CreateList("テスト", "")
token := l.ShareToken

req := httptest.NewRequest("POST", "/lists/"+token+"/items/nonexistent-id/toggle-required", nil)
Expand All @@ -335,7 +335,7 @@ func TestToggleRequiredInvalidItem(t *testing.T) {

func TestUpdateAssigneeInvalidItem(t *testing.T) {
mux, store := setupTestServer()
l := store.CreateList("テスト", "")
l, _ := store.CreateList("テスト", "")
token := l.ShareToken

form := url.Values{"assignee": {"花子"}}
Expand All @@ -351,7 +351,7 @@ func TestUpdateAssigneeInvalidItem(t *testing.T) {

func TestDeleteItemInvalidItem(t *testing.T) {
mux, store := setupTestServer()
l := store.CreateList("テスト", "")
l, _ := store.CreateList("テスト", "")
store.AddItem(l.ShareToken, "アイテム", "", true)
token := l.ShareToken

Expand Down Expand Up @@ -471,7 +471,7 @@ func TestCreateListDescriptionTruncation(t *testing.T) {

func TestAddItemNameTruncation(t *testing.T) {
mux, store := setupTestServer()
l := store.CreateList("テスト", "")
l, _ := store.CreateList("テスト", "")
token := l.ShareToken

longName := strings.Repeat("い", 150)
Expand All @@ -493,7 +493,7 @@ func TestAddItemNameTruncation(t *testing.T) {

func TestAddItemAssigneeTruncation(t *testing.T) {
mux, store := setupTestServer()
l := store.CreateList("テスト", "")
l, _ := store.CreateList("テスト", "")
token := l.ShareToken

longAssignee := strings.Repeat("う", 80)
Expand All @@ -512,7 +512,7 @@ func TestAddItemAssigneeTruncation(t *testing.T) {

func TestUpdateAssigneeTruncation(t *testing.T) {
mux, store := setupTestServer()
l := store.CreateList("テスト", "")
l, _ := store.CreateList("テスト", "")
store.AddItem(l.ShareToken, "アイテム", "太郎", true)

item := store.GetList(l.ShareToken).Items[0]
Expand Down Expand Up @@ -665,7 +665,6 @@ func TestInvalidTokenFormat(t *testing.T) {
mux, _ := setupTestServer()
invalidTokens := []string{
"short",
"../../../etc/passwd",
"nonexistent-token",
"AABBCCDD11223344AABBCCDD11223344",
"gg112233445566778899aabbccddeeff",
Expand Down
5 changes: 4 additions & 1 deletion main.go
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,10 @@ func buildShareURL(r *http.Request, token string) string {
}

func main() {
store := NewStore()
store := NewStore(
getEnvInt("MAX_LISTS", 1000),
getEnvInt("MAX_ITEMS_PER_LIST", 200),
)
mux := http.NewServeMux()
registerRoutes(mux, store)

Expand Down
36 changes: 26 additions & 10 deletions store.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,19 @@ import (

// Store is a thread-safe in-memory store for lists.
type Store struct {
mu sync.RWMutex
lists map[string]*List // keyed by ShareToken
nextID int
mu sync.RWMutex
lists map[string]*List // keyed by ShareToken
nextID int
maxLists int
maxItemsPerList int
}

func NewStore() *Store {
return &Store{lists: make(map[string]*List)}
func NewStore(maxLists, maxItemsPerList int) *Store {
return &Store{
lists: make(map[string]*List),
maxLists: maxLists,
maxItemsPerList: maxItemsPerList,
}
}

func (s *Store) genID() string {
Expand All @@ -32,10 +38,17 @@ func generateToken() string {
return hex.EncodeToString(b)
}

func (s *Store) CreateList(title, description string) *List {
var errMaxListsReached = fmt.Errorf("リスト数が上限に達しています")
var errMaxItemsReached = fmt.Errorf("アイテム数が上限に達しています")

func (s *Store) CreateList(title, description string) (*List, error) {
s.mu.Lock()
defer s.mu.Unlock()

if s.maxLists > 0 && len(s.lists) >= s.maxLists {
return nil, errMaxListsReached
}

l := &List{
ID: s.genID(),
Title: title,
Expand All @@ -45,7 +58,7 @@ func (s *Store) CreateList(title, description string) *List {
CreatedAt: time.Now(),
}
s.lists[l.ShareToken] = l
return l
return l, nil
}

func (s *Store) GetList(token string) *List {
Expand All @@ -54,13 +67,16 @@ func (s *Store) GetList(token string) *List {
return s.lists[token]
}

func (s *Store) AddItem(token, name, assignee string, required bool) *Item {
func (s *Store) AddItem(token, name, assignee string, required bool) (*Item, error) {
s.mu.Lock()
defer s.mu.Unlock()

l := s.lists[token]
if l == nil {
return nil
return nil, nil
}
if s.maxItemsPerList > 0 && len(l.Items) >= s.maxItemsPerList {
return nil, errMaxItemsReached
}
item := &Item{
ID: s.genID(),
Expand All @@ -70,7 +86,7 @@ func (s *Store) AddItem(token, name, assignee string, required bool) *Item {
UpdatedAt: time.Now(),
}
l.Items = append(l.Items, item)
return item
return item, nil
}

func (s *Store) findItem(token, itemID string) *Item {
Expand Down
Loading
Loading