feat: add Stow prototype iOS app and Gin backend
This commit is contained in:
@@ -0,0 +1,135 @@
|
||||
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())
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user