package api import ( "bytes" "context" "database/sql" "encoding/json" "net/http" "net/http/httptest" "testing" "time" "stow/backend/internal/store" ) type fakeStore struct { created bool lastUser string lastItem store.NewItem } func (f *fakeStore) CreateAnonymousSession(context.Context) (string, string, error) { return "user-1", "token-1", nil } func (f *fakeStore) Authenticate(_ context.Context, token string) (string, error) { if token != "token-1" { return "", sql.ErrNoRows } return "user-1", nil } func (f *fakeStore) CreateItem(_ context.Context, userID string, item store.NewItem) (store.Item, error) { f.created, f.lastUser, f.lastItem = true, userID, item return store.Item{ID: "item-1", SourceType: item.SourceType, PrimaryType: item.PrimaryType, Title: item.Title, Summary: item.Summary, Status: "ready", Tags: item.Tags, CreatedAt: time.Now()}, nil } func (f *fakeStore) GetItem(context.Context, string, string) (store.Item, error) { return store.Item{}, store.ErrNotFound } func (f *fakeStore) ListItems(context.Context, string, int) ([]store.Item, error) { return []store.Item{}, nil } func (f *fakeStore) SearchItems(context.Context, string, string, int) ([]store.Item, error) { return []store.Item{}, nil } func (f *fakeStore) UpdateItem(context.Context, string, string, store.ItemPatch) (store.Item, error) { return store.Item{}, store.ErrNotFound } func (f *fakeStore) DeleteItem(context.Context, string, string) error { return store.ErrNotFound } func TestItemsRequireSession(t *testing.T) { fake := &fakeStore{} request := httptest.NewRequest(http.MethodPost, "/v1/items", bytes.NewBufferString(`{"source_type":"manual_text","title":"咖啡店"}`)) response := httptest.NewRecorder() New(fake).ServeHTTP(response, request) if response.Code != http.StatusUnauthorized { t.Fatalf("status = %d, want 401", response.Code) } if fake.created { t.Fatal("item was created without authentication") } } func TestCreateItemValidatesAndUsesAuthenticatedUser(t *testing.T) { fake := &fakeStore{} handler := New(fake) for _, test := range []struct { name, body string want int }{ {"invalid source", `{"source_type":"unknown","title":"咖啡店"}`, http.StatusBadRequest}, {"valid item", `{"source_type":"manual_text","title":"咖啡店","tags":["上海"]}`, http.StatusCreated}, } { t.Run(test.name, func(t *testing.T) { request := httptest.NewRequest(http.MethodPost, "/v1/items", bytes.NewBufferString(test.body)) request.Header.Set("Authorization", "Bearer token-1") response := httptest.NewRecorder() handler.ServeHTTP(response, request) if response.Code != test.want { t.Fatalf("status = %d, want %d: %s", response.Code, test.want, response.Body.String()) } }) } if !fake.created || fake.lastUser != "user-1" || fake.lastItem.Title != "咖啡店" { t.Fatalf("wrong authenticated create: %+v", fake) } } func TestAnonymousSessionReturnsToken(t *testing.T) { response := httptest.NewRecorder() New(&fakeStore{}).ServeHTTP(response, httptest.NewRequest(http.MethodPost, "/v1/session/anonymous", nil)) if response.Code != http.StatusCreated { t.Fatalf("status = %d", response.Code) } var body struct { Code string `json:"code"` Message string `json:"message"` Data map[string]string `json:"data"` } if err := json.Unmarshal(response.Body.Bytes(), &body); err != nil { t.Fatal(err) } if body.Code != "ok" || body.Data["access_token"] != "token-1" || body.Data["token_type"] != "Bearer" { t.Fatalf("unexpected response: %v", body) } } func TestAllResponsesUseEnvelope(t *testing.T) { handler := New(&fakeStore{}) for _, test := range []struct { method string path string want int }{ {http.MethodGet, "/v1/health", http.StatusOK}, {http.MethodGet, "/v1/items", http.StatusUnauthorized}, {http.MethodGet, "/missing", http.StatusNotFound}, {http.MethodPut, "/v1/health", http.StatusMethodNotAllowed}, } { response := httptest.NewRecorder() handler.ServeHTTP(response, httptest.NewRequest(test.method, test.path, nil)) if response.Code != test.want { t.Fatalf("%s %s: status %d, want %d", test.method, test.path, response.Code, test.want) } var body struct { Code string `json:"code"` Message string `json:"message"` Data json.RawMessage `json:"data"` } if err := json.Unmarshal(response.Body.Bytes(), &body); err != nil { t.Fatalf("%s %s: %v", test.method, test.path, err) } if body.Code == "" || body.Message == "" || body.Data == nil { t.Fatalf("%s %s: invalid envelope: %s", test.method, test.path, response.Body.String()) } } }