238 lines
5.2 KiB
Go
238 lines
5.2 KiB
Go
package handler
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"reflect"
|
|
"sort"
|
|
"strings"
|
|
"sync"
|
|
"testing"
|
|
|
|
"5803025048/W04/internal/middleware"
|
|
"5803025048/W04/internal/models"
|
|
"5803025048/W04/internal/store"
|
|
)
|
|
|
|
// ---------- fake stores (in-memory, buatan tangan) ----------
|
|
|
|
type fakeUserStore struct {
|
|
mu sync.Mutex
|
|
users map[string]*models.User // key: username
|
|
createErr error
|
|
getErr error
|
|
}
|
|
|
|
func newFakeUserStore() *fakeUserStore {
|
|
return &fakeUserStore{users: map[string]*models.User{}}
|
|
}
|
|
|
|
func (f *fakeUserStore) Create(_ context.Context, user *models.User) error {
|
|
f.mu.Lock()
|
|
defer f.mu.Unlock()
|
|
if f.createErr != nil {
|
|
return f.createErr
|
|
}
|
|
if _, exists := f.users[user.Username]; exists {
|
|
return store.ErrUsernameTaken
|
|
}
|
|
cp := *user
|
|
f.users[user.Username] = &cp
|
|
return nil
|
|
}
|
|
|
|
func (f *fakeUserStore) GetByUsername(_ context.Context, username string) (*models.User, error) {
|
|
f.mu.Lock()
|
|
defer f.mu.Unlock()
|
|
if f.getErr != nil {
|
|
return nil, f.getErr
|
|
}
|
|
u, ok := f.users[username]
|
|
if !ok {
|
|
return nil, store.ErrNotFound
|
|
}
|
|
cp := *u
|
|
return &cp, nil
|
|
}
|
|
|
|
type fakeSessionStore struct {
|
|
mu sync.Mutex
|
|
sessions map[string]*models.Session // key: token
|
|
createErr error
|
|
getErr error
|
|
deleteErr error
|
|
}
|
|
|
|
func newFakeSessionStore() *fakeSessionStore {
|
|
return &fakeSessionStore{sessions: map[string]*models.Session{}}
|
|
}
|
|
|
|
func (f *fakeSessionStore) Create(_ context.Context, session *models.Session) error {
|
|
f.mu.Lock()
|
|
defer f.mu.Unlock()
|
|
if f.createErr != nil {
|
|
return f.createErr
|
|
}
|
|
cp := *session
|
|
f.sessions[session.Token] = &cp
|
|
return nil
|
|
}
|
|
|
|
func (f *fakeSessionStore) Get(_ context.Context, token string) (*models.Session, error) {
|
|
f.mu.Lock()
|
|
defer f.mu.Unlock()
|
|
if f.getErr != nil {
|
|
return nil, f.getErr
|
|
}
|
|
s, ok := f.sessions[token]
|
|
if !ok {
|
|
return nil, store.ErrNotFound
|
|
}
|
|
cp := *s
|
|
return &cp, nil
|
|
}
|
|
|
|
func (f *fakeSessionStore) Delete(_ context.Context, token string) error {
|
|
f.mu.Lock()
|
|
defer f.mu.Unlock()
|
|
if f.deleteErr != nil {
|
|
return f.deleteErr
|
|
}
|
|
delete(f.sessions, token)
|
|
return nil
|
|
}
|
|
|
|
func (f *fakeSessionStore) has(token string) bool {
|
|
f.mu.Lock()
|
|
defer f.mu.Unlock()
|
|
_, ok := f.sessions[token]
|
|
return ok
|
|
}
|
|
|
|
type fakeSnippetStore struct {
|
|
mu sync.Mutex
|
|
snippets map[string]*models.Snippet // key: id
|
|
createErr error
|
|
getErr error
|
|
listErr error
|
|
deleteErr error
|
|
}
|
|
|
|
func newFakeSnippetStore() *fakeSnippetStore {
|
|
return &fakeSnippetStore{snippets: map[string]*models.Snippet{}}
|
|
}
|
|
|
|
func (f *fakeSnippetStore) seed(id, title, content, userID string) {
|
|
f.mu.Lock()
|
|
defer f.mu.Unlock()
|
|
f.snippets[id] = &models.Snippet{ID: id, Title: title, Content: content, UserID: userID}
|
|
}
|
|
|
|
func (f *fakeSnippetStore) Create(_ context.Context, snippet *models.Snippet) error {
|
|
f.mu.Lock()
|
|
defer f.mu.Unlock()
|
|
if f.createErr != nil {
|
|
return f.createErr
|
|
}
|
|
cp := *snippet
|
|
f.snippets[snippet.ID] = &cp
|
|
return nil
|
|
}
|
|
|
|
func (f *fakeSnippetStore) GetByID(_ context.Context, id string) (*models.Snippet, error) {
|
|
f.mu.Lock()
|
|
defer f.mu.Unlock()
|
|
if f.getErr != nil {
|
|
return nil, f.getErr
|
|
}
|
|
s, ok := f.snippets[id]
|
|
if !ok {
|
|
return nil, store.ErrNotFound
|
|
}
|
|
cp := *s
|
|
return &cp, nil
|
|
}
|
|
|
|
// GetByUserID sengaja mengembalikan nil (bukan slice kosong) jika tidak ada hasil,
|
|
// supaya handler terbukti menghasilkan "[]" dan bukan "null".
|
|
func (f *fakeSnippetStore) GetByUserID(_ context.Context, userID string) ([]*models.Snippet, error) {
|
|
f.mu.Lock()
|
|
defer f.mu.Unlock()
|
|
if f.listErr != nil {
|
|
return nil, f.listErr
|
|
}
|
|
var out []*models.Snippet
|
|
for _, s := range f.snippets {
|
|
if s.UserID == userID {
|
|
cp := *s
|
|
out = append(out, &cp)
|
|
}
|
|
}
|
|
return out, nil
|
|
}
|
|
|
|
func (f *fakeSnippetStore) Delete(_ context.Context, id string) error {
|
|
f.mu.Lock()
|
|
defer f.mu.Unlock()
|
|
if f.deleteErr != nil {
|
|
return f.deleteErr
|
|
}
|
|
if _, ok := f.snippets[id]; !ok {
|
|
return store.ErrNotFound
|
|
}
|
|
delete(f.snippets, id)
|
|
return nil
|
|
}
|
|
|
|
func (f *fakeSnippetStore) has(id string) bool {
|
|
f.mu.Lock()
|
|
defer f.mu.Unlock()
|
|
_, ok := f.snippets[id]
|
|
return ok
|
|
}
|
|
|
|
// ---------- helper tes ----------
|
|
|
|
func newJSONRequest(method, target, body string) *http.Request {
|
|
req := httptest.NewRequest(method, target, strings.NewReader(body))
|
|
req.Header.Set("Content-Type", "application/json")
|
|
return req
|
|
}
|
|
|
|
func withUser(req *http.Request, userID string) *http.Request {
|
|
return req.WithContext(context.WithValue(req.Context(), middleware.UserIDKey, userID))
|
|
}
|
|
|
|
func assertJSONContentType(t *testing.T, rec *httptest.ResponseRecorder) {
|
|
t.Helper()
|
|
if ct := rec.Header().Get("Content-Type"); !strings.HasPrefix(ct, "application/json") {
|
|
t.Fatalf("Content-Type = %q, want application/json", ct)
|
|
}
|
|
}
|
|
|
|
func decodeMap(t *testing.T, raw []byte) map[string]any {
|
|
t.Helper()
|
|
var m map[string]any
|
|
if err := json.Unmarshal(raw, &m); err != nil {
|
|
t.Fatalf("body is not a JSON object: %v (body=%q)", err, string(raw))
|
|
}
|
|
return m
|
|
}
|
|
|
|
func assertKeys(t *testing.T, m map[string]any, want ...string) {
|
|
t.Helper()
|
|
got := make([]string, 0, len(m))
|
|
for k := range m {
|
|
got = append(got, k)
|
|
}
|
|
sort.Strings(got)
|
|
sort.Strings(want)
|
|
if !reflect.DeepEqual(got, want) {
|
|
t.Fatalf("keys = %v, want exactly %v", got, want)
|
|
}
|
|
}
|
|
|
|
func errNotFoundForTest() error { return store.ErrNotFound }
|