| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282 |
- // SPDX-FileCopyrightText: Copyright The Miniflux Authors. All rights reserved.
- // SPDX-License-Identifier: Apache-2.0
- package readeck
- import (
- "encoding/json"
- "io"
- "mime/multipart"
- "net/http"
- "net/http/httptest"
- "strings"
- "testing"
- "miniflux.app/v2/internal/config"
- )
- func TestCreateBookmark(t *testing.T) {
- configureIntegrationAllowPrivateNetworksOption(t)
- 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)
- }
- 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)
- }
- _, after, ok := strings.Cut(ct, "boundary=")
- if !ok {
- t.Fatalf("missing multipart boundary in Content-Type: %s", ct)
- }
- boundary := after
- mr := multipart.NewReader(r.Body, boundary)
- 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)
- before, after, ok := strings.Cut(all, "\n")
- if !ok {
- t.Fatalf("resource content missing header separator")
- }
- headerJSON := before
- resourceBody = after
- 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)
- }
- 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"
- }
- 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)
- }
- })
- }
- }
- func TestNewClient(t *testing.T) {
- 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)
- }
- }
- func configureIntegrationAllowPrivateNetworksOption(t *testing.T) {
- t.Helper()
- t.Setenv("INTEGRATION_ALLOW_PRIVATE_NETWORKS", "1")
- configParser := config.NewConfigParser()
- parsedOptions, err := configParser.ParseEnvironmentVariables()
- if err != nil {
- t.Fatalf("Unable to configure test options: %v", err)
- }
- previousOptions := config.Opts
- config.Opts = parsedOptions
- t.Cleanup(func() {
- config.Opts = previousOptions
- })
- }
|