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
17 changes: 17 additions & 0 deletions handler.go
Original file line number Diff line number Diff line change
@@ -1,12 +1,17 @@
package main

import (
"encoding/json"
"html/template"
"log/slog"
"net/http"
"strings"
"time"
)

// startTime はサーバー起動時刻を記録する。
var startTime = time.Now()

var tmpl = template.Must(template.New("").Funcs(template.FuncMap{
"progress": func(items []*Item) int {
if len(items) == 0 {
Expand All @@ -23,6 +28,7 @@ var tmpl = template.Must(template.New("").Funcs(template.FuncMap{
}).ParseGlob("templates/*.html"))

func registerRoutes(mux *http.ServeMux, store *Store) {
mux.HandleFunc("GET /health", handleHealth)
mux.HandleFunc("GET /", handleIndex)
mux.HandleFunc("POST /lists", handleCreateList(store))
mux.HandleFunc("GET /lists/{token}", handleShowList(store))
Expand All @@ -34,6 +40,17 @@ func registerRoutes(mux *http.ServeMux, store *Store) {
mux.HandleFunc("POST /lists/{token}/delete", handleDeleteList(store))
}

func handleHealth(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
json.NewEncoder(w).Encode(map[string]string{ //nolint:errcheck
"status": "ok",
"service": "bringit",
"uptime": time.Since(startTime).Round(time.Second).String(),
"timestamp": time.Now().UTC().Format(time.RFC3339),
})
}

func handleIndex(w http.ResponseWriter, r *http.Request) {
if r.URL.Path != "/" {
http.NotFound(w, r)
Expand Down
28 changes: 28 additions & 0 deletions handler_test.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package main

import (
"encoding/json"
"net/http"
"net/http/httptest"
"net/url"
Expand All @@ -15,6 +16,33 @@ func setupTestServer() (*http.ServeMux, *Store) {
return mux, store
}

func TestHealthEndpoint(t *testing.T) {
mux, _ := setupTestServer()
req := httptest.NewRequest("GET", "/health", nil)
w := httptest.NewRecorder()
mux.ServeHTTP(w, req)

if w.Code != http.StatusOK {
t.Fatalf("expected 200, got %d", w.Code)
}
if ct := w.Header().Get("Content-Type"); !strings.HasPrefix(ct, "application/json") {
t.Fatalf("expected application/json content-type, got %s", ct)
}
var body map[string]string
if err := json.NewDecoder(w.Body).Decode(&body); err != nil {
t.Fatalf("failed to decode JSON: %v", err)
}
if body["status"] != "ok" {
t.Fatalf("expected status=ok, got %s", body["status"])
}
if body["service"] != "bringit" {
t.Fatalf("expected service=bringit, got %s", body["service"])
}
if body["timestamp"] == "" {
t.Fatal("expected non-empty timestamp")
}
}

func TestIndexPage(t *testing.T) {
mux, _ := setupTestServer()
req := httptest.NewRequest("GET", "/", nil)
Expand Down
172 changes: 172 additions & 0 deletions store_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,172 @@
package main

import (
"os"
"sync"
"testing"
)

func TestNewStore(t *testing.T) {
s := NewStore()
if s == nil {
t.Fatal("NewStore returned nil")
}
if s.lists == nil {
t.Fatal("store.lists is nil")
}
}

func TestCreateListAndGetList(t *testing.T) {
s := NewStore()
l := s.CreateList("テスト", "説明")
if l == nil {
t.Fatal("CreateList returned nil")
}
if l.Title != "テスト" {
t.Fatalf("expected title=テスト, got %s", l.Title)
}
if l.Description != "説明" {
t.Fatalf("expected description=説明, got %s", l.Description)
}
if l.ShareToken == "" {
t.Fatal("ShareToken must not be empty")
}
if l.Items == nil {
t.Fatal("Items must be initialized (not nil)")
}

got := s.GetList(l.ShareToken)
if got == nil {
t.Fatal("GetList returned nil for valid token")
}
if got.Title != l.Title {
t.Fatalf("expected title=%s, got %s", l.Title, got.Title)
}
}

func TestGetListNonExistent(t *testing.T) {
s := NewStore()
if s.GetList("nonexistent") != nil {
t.Fatal("expected nil for non-existent token")
}
}

func TestAddItemToNonExistentList(t *testing.T) {
s := NewStore()
item := s.AddItem("nonexistent", "アイテム", "", true)
if item != nil {
t.Fatal("expected nil when adding item to non-existent list")
}
}

func TestAddItemAndRetrieve(t *testing.T) {
s := NewStore()
l := s.CreateList("リスト", "")
item := s.AddItem(l.ShareToken, "テントが必要", "太郎", true)
if item == nil {
t.Fatal("AddItem returned nil")
}
if item.Name != "テントが必要" {
t.Fatalf("expected name=テントが必要, got %s", item.Name)
}
if item.Assignee != "太郎" {
t.Fatalf("expected assignee=太郎, got %s", item.Assignee)
}
if !item.Required {
t.Fatal("expected Required=true")
}
if item.Prepared {
t.Fatal("expected Prepared=false initially")
}
}

func TestDeleteListReturnValues(t *testing.T) {
s := NewStore()
l := s.CreateList("削除テスト", "")
token := l.ShareToken

// 存在するリストの削除 → true
if !s.DeleteList(token) {
t.Fatal("expected DeleteList to return true for existing list")
}
// 再度削除 → false
if s.DeleteList(token) {
t.Fatal("expected DeleteList to return false for already-deleted list")
}
// GetListは nil を返す
if s.GetList(token) != nil {
t.Fatal("expected nil after deletion")
}
}

func TestUniqueTokens(t *testing.T) {
s := NewStore()
tokens := make(map[string]struct{})
for i := 0; i < 100; i++ {
l := s.CreateList("リスト", "")
if _, dup := tokens[l.ShareToken]; dup {
t.Fatalf("duplicate token generated: %s", l.ShareToken)
}
tokens[l.ShareToken] = struct{}{}
}
}

func TestConcurrentCreateList(t *testing.T) {
s := NewStore()
const goroutines = 50
var wg sync.WaitGroup
wg.Add(goroutines)
tokens := make([]string, goroutines)

for i := 0; i < goroutines; i++ {
go func(idx int) {
defer wg.Done()
l := s.CreateList("並行テスト", "")
tokens[idx] = l.ShareToken
}(i)
}
wg.Wait()

// 全トークンが取得可能であることを確認
for _, tok := range tokens {
if s.GetList(tok) == nil {
t.Fatalf("list with token %s not found after concurrent creation", tok)
}
}
}

func TestConcurrentAddItem(t *testing.T) {
s := NewStore()
l := s.CreateList("並行アイテム追加", "")
const goroutines = 50
var wg sync.WaitGroup
wg.Add(goroutines)

for i := 0; i < goroutines; i++ {
go func() {
defer wg.Done()
s.AddItem(l.ShareToken, "アイテム", "", true)
}()
}
wg.Wait()

got := s.GetList(l.ShareToken)
if len(got.Items) != goroutines {
t.Fatalf("expected %d items, got %d", goroutines, len(got.Items))
}
}

func TestGetEnv(t *testing.T) {
// 環境変数が設定されている場合
os.Setenv("TEST_VAR", "hello")
defer os.Unsetenv("TEST_VAR")
if v := getEnv("TEST_VAR", "default"); v != "hello" {
t.Fatalf("expected hello, got %s", v)
}

// 環境変数が未設定の場合
os.Unsetenv("TEST_VAR")
if v := getEnv("TEST_VAR", "default"); v != "default" {
t.Fatalf("expected default, got %s", v)
}
}
Loading