|
|
@@ -4,257 +4,257 @@
|
|
|
package readeck
|
|
|
|
|
|
import (
|
|
|
- "encoding/json"
|
|
|
- "io"
|
|
|
- "mime/multipart"
|
|
|
- "net/http"
|
|
|
- "net/http/httptest"
|
|
|
- "strings"
|
|
|
- "testing"
|
|
|
+ "encoding/json"
|
|
|
+ "io"
|
|
|
+ "mime/multipart"
|
|
|
+ "net/http"
|
|
|
+ "net/http/httptest"
|
|
|
+ "strings"
|
|
|
+ "testing"
|
|
|
)
|
|
|
|
|
|
func TestCreateBookmark(t *testing.T) {
|
|
|
- entryURL := "https://example.com/article"
|
|
|
- entryTitle := "Example Title"
|
|
|
- entryContent := "<p>Some HTML content</p>"
|
|
|
- labels := "tag1,tag2"
|
|
|
+ entryURL := "https://example.com/article"
|
|
|
+ entryTitle := "Example Title"
|
|
|
+ entryContent := "<p>Some HTML content</p>"
|
|
|
+ labels := "tag1,tag2"
|
|
|
|
|
|
- tests := []struct {
|
|
|
- name string
|
|
|
- onlyURL bool
|
|
|
- baseURL string
|
|
|
- apiKey string
|
|
|
- labels string
|
|
|
- entryURL string
|
|
|
- entryTitle string
|
|
|
- entryContent string
|
|
|
- serverResponse func(w http.ResponseWriter, r *http.Request)
|
|
|
- wantErr bool
|
|
|
- errContains string
|
|
|
- }{
|
|
|
- {
|
|
|
- name: "successful bookmark creation with only URL",
|
|
|
- onlyURL: true,
|
|
|
- labels: labels,
|
|
|
- entryURL: entryURL,
|
|
|
- entryTitle: entryTitle,
|
|
|
- entryContent: entryContent,
|
|
|
- serverResponse: func(w http.ResponseWriter, r *http.Request) {
|
|
|
- if r.Method != http.MethodPost {
|
|
|
- t.Errorf("expected POST, got %s", r.Method)
|
|
|
- }
|
|
|
- if r.URL.Path != "/api/bookmarks/" {
|
|
|
- t.Errorf("expected path /api/bookmarks/, got %s", r.URL.Path)
|
|
|
- }
|
|
|
- if got := r.Header.Get("Authorization"); !strings.HasPrefix(got, "Bearer ") {
|
|
|
- t.Errorf("expected Authorization Bearer header, got %q", got)
|
|
|
- }
|
|
|
- if ct := r.Header.Get("Content-Type"); ct != "application/json" {
|
|
|
- t.Errorf("expected Content-Type application/json, got %s", ct)
|
|
|
- }
|
|
|
+ tests := []struct {
|
|
|
+ name string
|
|
|
+ onlyURL bool
|
|
|
+ baseURL string
|
|
|
+ apiKey string
|
|
|
+ labels string
|
|
|
+ entryURL string
|
|
|
+ entryTitle string
|
|
|
+ entryContent string
|
|
|
+ serverResponse func(w http.ResponseWriter, r *http.Request)
|
|
|
+ wantErr bool
|
|
|
+ errContains string
|
|
|
+ }{
|
|
|
+ {
|
|
|
+ name: "successful bookmark creation with only URL",
|
|
|
+ onlyURL: true,
|
|
|
+ labels: labels,
|
|
|
+ entryURL: entryURL,
|
|
|
+ entryTitle: entryTitle,
|
|
|
+ entryContent: entryContent,
|
|
|
+ serverResponse: func(w http.ResponseWriter, r *http.Request) {
|
|
|
+ if r.Method != http.MethodPost {
|
|
|
+ t.Errorf("expected POST, got %s", r.Method)
|
|
|
+ }
|
|
|
+ if r.URL.Path != "/api/bookmarks/" {
|
|
|
+ t.Errorf("expected path /api/bookmarks/, got %s", r.URL.Path)
|
|
|
+ }
|
|
|
+ if got := r.Header.Get("Authorization"); !strings.HasPrefix(got, "Bearer ") {
|
|
|
+ t.Errorf("expected Authorization Bearer header, got %q", got)
|
|
|
+ }
|
|
|
+ if ct := r.Header.Get("Content-Type"); ct != "application/json" {
|
|
|
+ t.Errorf("expected Content-Type application/json, got %s", ct)
|
|
|
+ }
|
|
|
|
|
|
- body, _ := io.ReadAll(r.Body)
|
|
|
- var payload map[string]any
|
|
|
- if err := json.Unmarshal(body, &payload); err != nil {
|
|
|
- t.Fatalf("failed to parse JSON body: %v", err)
|
|
|
- }
|
|
|
- if u := payload["url"]; u != entryURL {
|
|
|
- t.Errorf("expected url %s, got %v", entryURL, u)
|
|
|
- }
|
|
|
- if title := payload["title"]; title != entryTitle {
|
|
|
- t.Errorf("expected title %s, got %v", entryTitle, title)
|
|
|
- }
|
|
|
- // Labels should be split into an array
|
|
|
- if raw := payload["labels"]; raw == nil {
|
|
|
- t.Errorf("expected labels to be set")
|
|
|
- } else if arr, ok := raw.([]any); ok {
|
|
|
- if len(arr) != 2 || arr[0] != "tag1" || arr[1] != "tag2" {
|
|
|
- t.Errorf("unexpected labels: %#v", arr)
|
|
|
- }
|
|
|
- } else {
|
|
|
- t.Errorf("labels should be an array, got %T", raw)
|
|
|
- }
|
|
|
- w.WriteHeader(http.StatusOK)
|
|
|
- },
|
|
|
- },
|
|
|
- {
|
|
|
- name: "successful bookmark creation with content (multipart)",
|
|
|
- onlyURL: false,
|
|
|
- labels: labels,
|
|
|
- entryURL: entryURL,
|
|
|
- entryTitle: entryTitle,
|
|
|
- entryContent: entryContent,
|
|
|
- serverResponse: func(w http.ResponseWriter, r *http.Request) {
|
|
|
- if r.Method != http.MethodPost {
|
|
|
- t.Errorf("expected POST, got %s", r.Method)
|
|
|
- }
|
|
|
- if r.URL.Path != "/api/bookmarks/" {
|
|
|
- t.Errorf("expected path /api/bookmarks/, got %s", r.URL.Path)
|
|
|
- }
|
|
|
- if got := r.Header.Get("Authorization"); !strings.HasPrefix(got, "Bearer ") {
|
|
|
- t.Errorf("expected Authorization Bearer header, got %q", got)
|
|
|
- }
|
|
|
- ct := r.Header.Get("Content-Type")
|
|
|
- if !strings.HasPrefix(ct, "multipart/form-data;") {
|
|
|
- t.Errorf("expected multipart/form-data, got %s", ct)
|
|
|
- }
|
|
|
- boundaryIdx := strings.Index(ct, "boundary=")
|
|
|
- if boundaryIdx == -1 {
|
|
|
- t.Fatalf("missing multipart boundary in Content-Type: %s", ct)
|
|
|
- }
|
|
|
- boundary := ct[boundaryIdx+len("boundary="):]
|
|
|
- mr := multipart.NewReader(r.Body, boundary)
|
|
|
+ body, _ := io.ReadAll(r.Body)
|
|
|
+ var payload map[string]any
|
|
|
+ if err := json.Unmarshal(body, &payload); err != nil {
|
|
|
+ t.Fatalf("failed to parse JSON body: %v", err)
|
|
|
+ }
|
|
|
+ if u := payload["url"]; u != entryURL {
|
|
|
+ t.Errorf("expected url %s, got %v", entryURL, u)
|
|
|
+ }
|
|
|
+ if title := payload["title"]; title != entryTitle {
|
|
|
+ t.Errorf("expected title %s, got %v", entryTitle, title)
|
|
|
+ }
|
|
|
+ // Labels should be split into an array
|
|
|
+ if raw := payload["labels"]; raw == nil {
|
|
|
+ t.Errorf("expected labels to be set")
|
|
|
+ } else if arr, ok := raw.([]any); ok {
|
|
|
+ if len(arr) != 2 || arr[0] != "tag1" || arr[1] != "tag2" {
|
|
|
+ t.Errorf("unexpected labels: %#v", arr)
|
|
|
+ }
|
|
|
+ } else {
|
|
|
+ t.Errorf("labels should be an array, got %T", raw)
|
|
|
+ }
|
|
|
+ w.WriteHeader(http.StatusOK)
|
|
|
+ },
|
|
|
+ },
|
|
|
+ {
|
|
|
+ name: "successful bookmark creation with content (multipart)",
|
|
|
+ onlyURL: false,
|
|
|
+ labels: labels,
|
|
|
+ entryURL: entryURL,
|
|
|
+ entryTitle: entryTitle,
|
|
|
+ entryContent: entryContent,
|
|
|
+ serverResponse: func(w http.ResponseWriter, r *http.Request) {
|
|
|
+ if r.Method != http.MethodPost {
|
|
|
+ t.Errorf("expected POST, got %s", r.Method)
|
|
|
+ }
|
|
|
+ if r.URL.Path != "/api/bookmarks/" {
|
|
|
+ t.Errorf("expected path /api/bookmarks/, got %s", r.URL.Path)
|
|
|
+ }
|
|
|
+ if got := r.Header.Get("Authorization"); !strings.HasPrefix(got, "Bearer ") {
|
|
|
+ t.Errorf("expected Authorization Bearer header, got %q", got)
|
|
|
+ }
|
|
|
+ ct := r.Header.Get("Content-Type")
|
|
|
+ if !strings.HasPrefix(ct, "multipart/form-data;") {
|
|
|
+ t.Errorf("expected multipart/form-data, got %s", ct)
|
|
|
+ }
|
|
|
+ boundaryIdx := strings.Index(ct, "boundary=")
|
|
|
+ if boundaryIdx == -1 {
|
|
|
+ t.Fatalf("missing multipart boundary in Content-Type: %s", ct)
|
|
|
+ }
|
|
|
+ boundary := ct[boundaryIdx+len("boundary="):]
|
|
|
+ mr := multipart.NewReader(r.Body, boundary)
|
|
|
|
|
|
- seenLabels := []string{}
|
|
|
- var seenURL, seenTitle, seenFeature string
|
|
|
- var resourceHeader map[string]any
|
|
|
- var resourceBody string
|
|
|
+ seenLabels := []string{}
|
|
|
+ var seenURL, seenTitle, seenFeature string
|
|
|
+ var resourceHeader map[string]any
|
|
|
+ var resourceBody string
|
|
|
|
|
|
- for {
|
|
|
- part, err := mr.NextPart()
|
|
|
- if err == io.EOF {
|
|
|
- break
|
|
|
- }
|
|
|
- if err != nil {
|
|
|
- t.Fatalf("reading multipart: %v", err)
|
|
|
- }
|
|
|
- name := part.FormName()
|
|
|
- data, _ := io.ReadAll(part)
|
|
|
- switch name {
|
|
|
- case "url":
|
|
|
- seenURL = string(data)
|
|
|
- case "title":
|
|
|
- seenTitle = string(data)
|
|
|
- case "feature_find_main":
|
|
|
- seenFeature = string(data)
|
|
|
- case "labels":
|
|
|
- seenLabels = append(seenLabels, string(data))
|
|
|
- case "resource":
|
|
|
- // First line is JSON header, then newline, then content
|
|
|
- all := string(data)
|
|
|
- idx := strings.IndexByte(all, '\n')
|
|
|
- if idx == -1 {
|
|
|
- t.Fatalf("resource content missing header separator")
|
|
|
- }
|
|
|
- headerJSON := all[:idx]
|
|
|
- resourceBody = all[idx+1:]
|
|
|
- if err := json.Unmarshal([]byte(headerJSON), &resourceHeader); err != nil {
|
|
|
- t.Fatalf("invalid resource header JSON: %v", err)
|
|
|
- }
|
|
|
- }
|
|
|
- }
|
|
|
+ for {
|
|
|
+ part, err := mr.NextPart()
|
|
|
+ if err == io.EOF {
|
|
|
+ break
|
|
|
+ }
|
|
|
+ if err != nil {
|
|
|
+ t.Fatalf("reading multipart: %v", err)
|
|
|
+ }
|
|
|
+ name := part.FormName()
|
|
|
+ data, _ := io.ReadAll(part)
|
|
|
+ switch name {
|
|
|
+ case "url":
|
|
|
+ seenURL = string(data)
|
|
|
+ case "title":
|
|
|
+ seenTitle = string(data)
|
|
|
+ case "feature_find_main":
|
|
|
+ seenFeature = string(data)
|
|
|
+ case "labels":
|
|
|
+ seenLabels = append(seenLabels, string(data))
|
|
|
+ case "resource":
|
|
|
+ // First line is JSON header, then newline, then content
|
|
|
+ all := string(data)
|
|
|
+ idx := strings.IndexByte(all, '\n')
|
|
|
+ if idx == -1 {
|
|
|
+ t.Fatalf("resource content missing header separator")
|
|
|
+ }
|
|
|
+ headerJSON := all[:idx]
|
|
|
+ resourceBody = all[idx+1:]
|
|
|
+ if err := json.Unmarshal([]byte(headerJSON), &resourceHeader); err != nil {
|
|
|
+ t.Fatalf("invalid resource header JSON: %v", err)
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
|
|
|
- if seenURL != entryURL {
|
|
|
- t.Errorf("expected url %s, got %s", entryURL, seenURL)
|
|
|
- }
|
|
|
- if seenTitle != entryTitle {
|
|
|
- t.Errorf("expected title %s, got %s", entryTitle, seenTitle)
|
|
|
- }
|
|
|
- if seenFeature != "false" {
|
|
|
- t.Errorf("expected feature_find_main to be 'false', got %s", seenFeature)
|
|
|
- }
|
|
|
- if len(seenLabels) != 2 || seenLabels[0] != "tag1" || seenLabels[1] != "tag2" {
|
|
|
- t.Errorf("unexpected labels: %#v", seenLabels)
|
|
|
- }
|
|
|
- if resourceHeader == nil {
|
|
|
- t.Fatalf("missing resource header")
|
|
|
- }
|
|
|
- if hURL, _ := resourceHeader["url"].(string); hURL != entryURL {
|
|
|
- t.Errorf("expected resource header url %s, got %v", entryURL, hURL)
|
|
|
- }
|
|
|
- if headers, ok := resourceHeader["headers"].(map[string]any); ok {
|
|
|
- if ct, _ := headers["content-type"].(string); ct != "text/html; charset=utf-8" {
|
|
|
- t.Errorf("expected resource header content-type text/html; charset=utf-8, got %v", ct)
|
|
|
- }
|
|
|
- } else {
|
|
|
- t.Errorf("missing resource header 'headers' field")
|
|
|
- }
|
|
|
- if resourceBody != entryContent {
|
|
|
- t.Errorf("expected resource body %q, got %q", entryContent, resourceBody)
|
|
|
- }
|
|
|
+ if seenURL != entryURL {
|
|
|
+ t.Errorf("expected url %s, got %s", entryURL, seenURL)
|
|
|
+ }
|
|
|
+ if seenTitle != entryTitle {
|
|
|
+ t.Errorf("expected title %s, got %s", entryTitle, seenTitle)
|
|
|
+ }
|
|
|
+ if seenFeature != "false" {
|
|
|
+ t.Errorf("expected feature_find_main to be 'false', got %s", seenFeature)
|
|
|
+ }
|
|
|
+ if len(seenLabels) != 2 || seenLabels[0] != "tag1" || seenLabels[1] != "tag2" {
|
|
|
+ t.Errorf("unexpected labels: %#v", seenLabels)
|
|
|
+ }
|
|
|
+ if resourceHeader == nil {
|
|
|
+ t.Fatalf("missing resource header")
|
|
|
+ }
|
|
|
+ if hURL, _ := resourceHeader["url"].(string); hURL != entryURL {
|
|
|
+ t.Errorf("expected resource header url %s, got %v", entryURL, hURL)
|
|
|
+ }
|
|
|
+ if headers, ok := resourceHeader["headers"].(map[string]any); ok {
|
|
|
+ if ct, _ := headers["content-type"].(string); ct != "text/html; charset=utf-8" {
|
|
|
+ t.Errorf("expected resource header content-type text/html; charset=utf-8, got %v", ct)
|
|
|
+ }
|
|
|
+ } else {
|
|
|
+ t.Errorf("missing resource header 'headers' field")
|
|
|
+ }
|
|
|
+ if resourceBody != entryContent {
|
|
|
+ t.Errorf("expected resource body %q, got %q", entryContent, resourceBody)
|
|
|
+ }
|
|
|
|
|
|
- w.WriteHeader(http.StatusOK)
|
|
|
- },
|
|
|
- },
|
|
|
- {
|
|
|
- name: "error when server returns 400",
|
|
|
- onlyURL: true,
|
|
|
- labels: labels,
|
|
|
- entryURL: entryURL,
|
|
|
- entryTitle: entryTitle,
|
|
|
- entryContent: entryContent,
|
|
|
- serverResponse: func(w http.ResponseWriter, r *http.Request) {
|
|
|
- w.WriteHeader(http.StatusBadRequest)
|
|
|
- },
|
|
|
- wantErr: true,
|
|
|
- errContains: "unable to create bookmark",
|
|
|
- },
|
|
|
- {
|
|
|
- name: "error when missing baseURL or apiKey",
|
|
|
- onlyURL: true,
|
|
|
- baseURL: "",
|
|
|
- apiKey: "",
|
|
|
- labels: labels,
|
|
|
- entryURL: entryURL,
|
|
|
- entryTitle: entryTitle,
|
|
|
- entryContent: entryContent,
|
|
|
- serverResponse: nil,
|
|
|
- wantErr: true,
|
|
|
- errContains: "missing base URL or API key",
|
|
|
- },
|
|
|
- }
|
|
|
+ w.WriteHeader(http.StatusOK)
|
|
|
+ },
|
|
|
+ },
|
|
|
+ {
|
|
|
+ name: "error when server returns 400",
|
|
|
+ onlyURL: true,
|
|
|
+ labels: labels,
|
|
|
+ entryURL: entryURL,
|
|
|
+ entryTitle: entryTitle,
|
|
|
+ entryContent: entryContent,
|
|
|
+ serverResponse: func(w http.ResponseWriter, r *http.Request) {
|
|
|
+ w.WriteHeader(http.StatusBadRequest)
|
|
|
+ },
|
|
|
+ wantErr: true,
|
|
|
+ errContains: "unable to create bookmark",
|
|
|
+ },
|
|
|
+ {
|
|
|
+ name: "error when missing baseURL or apiKey",
|
|
|
+ onlyURL: true,
|
|
|
+ baseURL: "",
|
|
|
+ apiKey: "",
|
|
|
+ labels: labels,
|
|
|
+ entryURL: entryURL,
|
|
|
+ entryTitle: entryTitle,
|
|
|
+ entryContent: entryContent,
|
|
|
+ serverResponse: nil,
|
|
|
+ wantErr: true,
|
|
|
+ errContains: "missing base URL or API key",
|
|
|
+ },
|
|
|
+ }
|
|
|
|
|
|
- for _, tt := range tests {
|
|
|
- t.Run(tt.name, func(t *testing.T) {
|
|
|
- var serverURL string
|
|
|
- if tt.serverResponse != nil {
|
|
|
- srv := httptest.NewServer(http.HandlerFunc(tt.serverResponse))
|
|
|
- defer srv.Close()
|
|
|
- serverURL = srv.URL
|
|
|
- }
|
|
|
- baseURL := tt.baseURL
|
|
|
- if baseURL == "" {
|
|
|
- baseURL = serverURL
|
|
|
- }
|
|
|
- apiKey := tt.apiKey
|
|
|
- if apiKey == "" {
|
|
|
- apiKey = "test-api-key"
|
|
|
- }
|
|
|
+ for _, tt := range tests {
|
|
|
+ t.Run(tt.name, func(t *testing.T) {
|
|
|
+ var serverURL string
|
|
|
+ if tt.serverResponse != nil {
|
|
|
+ srv := httptest.NewServer(http.HandlerFunc(tt.serverResponse))
|
|
|
+ defer srv.Close()
|
|
|
+ serverURL = srv.URL
|
|
|
+ }
|
|
|
+ baseURL := tt.baseURL
|
|
|
+ if baseURL == "" {
|
|
|
+ baseURL = serverURL
|
|
|
+ }
|
|
|
+ apiKey := tt.apiKey
|
|
|
+ if apiKey == "" {
|
|
|
+ apiKey = "test-api-key"
|
|
|
+ }
|
|
|
|
|
|
- client := NewClient(baseURL, apiKey, tt.labels, tt.onlyURL)
|
|
|
- err := client.CreateBookmark(tt.entryURL, tt.entryTitle, tt.entryContent)
|
|
|
+ client := NewClient(baseURL, apiKey, tt.labels, tt.onlyURL)
|
|
|
+ err := client.CreateBookmark(tt.entryURL, tt.entryTitle, tt.entryContent)
|
|
|
|
|
|
- if tt.wantErr {
|
|
|
- if err == nil {
|
|
|
- t.Fatalf("expected error, got none")
|
|
|
- }
|
|
|
- if tt.errContains != "" && !strings.Contains(err.Error(), tt.errContains) {
|
|
|
- t.Fatalf("expected error containing %q, got %q", tt.errContains, err.Error())
|
|
|
- }
|
|
|
- } else if err != nil {
|
|
|
- t.Fatalf("unexpected error: %v", err)
|
|
|
- }
|
|
|
- })
|
|
|
- }
|
|
|
+ if tt.wantErr {
|
|
|
+ if err == nil {
|
|
|
+ t.Fatalf("expected error, got none")
|
|
|
+ }
|
|
|
+ if tt.errContains != "" && !strings.Contains(err.Error(), tt.errContains) {
|
|
|
+ t.Fatalf("expected error containing %q, got %q", tt.errContains, err.Error())
|
|
|
+ }
|
|
|
+ } else if err != nil {
|
|
|
+ t.Fatalf("unexpected error: %v", err)
|
|
|
+ }
|
|
|
+ })
|
|
|
+ }
|
|
|
}
|
|
|
|
|
|
func TestNewClient(t *testing.T) {
|
|
|
- baseURL := "https://readeck.example.com"
|
|
|
- apiKey := "key"
|
|
|
- labels := "tag1,tag2"
|
|
|
- onlyURL := true
|
|
|
+ baseURL := "https://readeck.example.com"
|
|
|
+ apiKey := "key"
|
|
|
+ labels := "tag1,tag2"
|
|
|
+ onlyURL := true
|
|
|
|
|
|
- c := NewClient(baseURL, apiKey, labels, onlyURL)
|
|
|
- if c.baseURL != baseURL {
|
|
|
- t.Errorf("expected baseURL %s, got %s", baseURL, c.baseURL)
|
|
|
- }
|
|
|
- if c.apiKey != apiKey {
|
|
|
- t.Errorf("expected apiKey %s, got %s", apiKey, c.apiKey)
|
|
|
- }
|
|
|
- if c.labels != labels {
|
|
|
- t.Errorf("expected labels %s, got %s", labels, c.labels)
|
|
|
- }
|
|
|
- if c.onlyURL != onlyURL {
|
|
|
- t.Errorf("expected onlyURL %v, got %v", onlyURL, c.onlyURL)
|
|
|
- }
|
|
|
+ c := NewClient(baseURL, apiKey, labels, onlyURL)
|
|
|
+ if c.baseURL != baseURL {
|
|
|
+ t.Errorf("expected baseURL %s, got %s", baseURL, c.baseURL)
|
|
|
+ }
|
|
|
+ if c.apiKey != apiKey {
|
|
|
+ t.Errorf("expected apiKey %s, got %s", apiKey, c.apiKey)
|
|
|
+ }
|
|
|
+ if c.labels != labels {
|
|
|
+ t.Errorf("expected labels %s, got %s", labels, c.labels)
|
|
|
+ }
|
|
|
+ if c.onlyURL != onlyURL {
|
|
|
+ t.Errorf("expected onlyURL %v, got %v", onlyURL, c.onlyURL)
|
|
|
+ }
|
|
|
}
|