api_test.go 7.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236
  1. // SPDX-FileCopyrightText: Copyright The Miniflux Authors. All rights reserved.
  2. // SPDX-License-Identifier: Apache-2.0
  3. package api // import "miniflux.app/v2/internal/api"
  4. import (
  5. "encoding/json"
  6. "net/http"
  7. "net/http/httptest"
  8. "runtime"
  9. "testing"
  10. "miniflux.app/v2/internal/config"
  11. "miniflux.app/v2/internal/version"
  12. )
  13. func TestNewHandlerHandlesOptionsRequests(t *testing.T) {
  14. handler := NewHandler(nil, nil)
  15. r := httptest.NewRequest(http.MethodOptions, "/v1/users", nil)
  16. w := httptest.NewRecorder()
  17. handler.ServeHTTP(w, r)
  18. if got := w.Code; got != http.StatusNoContent {
  19. t.Fatalf(`Unexpected status code, got %d instead of %d`, got, http.StatusNoContent)
  20. }
  21. if got := w.Header().Get("Access-Control-Allow-Origin"); got != "*" {
  22. t.Fatalf(`Unexpected Access-Control-Allow-Origin header, got %q`, got)
  23. }
  24. if got := w.Header().Get("Access-Control-Allow-Methods"); got != "GET, POST, PUT, DELETE, OPTIONS" {
  25. t.Fatalf(`Unexpected Access-Control-Allow-Methods header, got %q`, got)
  26. }
  27. if got := w.Header().Get("Access-Control-Allow-Headers"); got != "X-Auth-Token, Authorization, Content-Type, Accept" {
  28. t.Fatalf(`Unexpected Access-Control-Allow-Headers header, got %q`, got)
  29. }
  30. if got := w.Header().Get("Access-Control-Max-Age"); got != "3600" {
  31. t.Fatalf(`Unexpected Access-Control-Max-Age header, got %q`, got)
  32. }
  33. }
  34. func TestVersionHandler(t *testing.T) {
  35. h := &handler{}
  36. r := httptest.NewRequest(http.MethodGet, "/v1/version", nil)
  37. w := httptest.NewRecorder()
  38. h.versionHandler(w, r)
  39. if got := w.Code; got != http.StatusOK {
  40. t.Fatalf(`Unexpected status code, got %d instead of %d`, got, http.StatusOK)
  41. }
  42. if got := w.Header().Get("Content-Type"); got != "application/json" {
  43. t.Fatalf(`Unexpected Content-Type header, got %q`, got)
  44. }
  45. var responseBody versionResponse
  46. if err := json.NewDecoder(w.Body).Decode(&responseBody); err != nil {
  47. t.Fatalf("Unexpected JSON decoding error: %v", err)
  48. }
  49. if responseBody.Version != version.Version {
  50. t.Fatalf(`Unexpected version, got %q instead of %q`, responseBody.Version, version.Version)
  51. }
  52. if responseBody.Commit != version.Commit {
  53. t.Fatalf(`Unexpected commit, got %q instead of %q`, responseBody.Commit, version.Commit)
  54. }
  55. if responseBody.BuildDate != version.BuildDate {
  56. t.Fatalf(`Unexpected build date, got %q instead of %q`, responseBody.BuildDate, version.BuildDate)
  57. }
  58. if responseBody.GoVersion != runtime.Version() {
  59. t.Fatalf(`Unexpected Go version, got %q instead of %q`, responseBody.GoVersion, runtime.Version())
  60. }
  61. if responseBody.Compiler != runtime.Compiler {
  62. t.Fatalf(`Unexpected compiler, got %q instead of %q`, responseBody.Compiler, runtime.Compiler)
  63. }
  64. if responseBody.Arch != runtime.GOARCH {
  65. t.Fatalf(`Unexpected architecture, got %q instead of %q`, responseBody.Arch, runtime.GOARCH)
  66. }
  67. if responseBody.OS != runtime.GOOS {
  68. t.Fatalf(`Unexpected OS, got %q instead of %q`, responseBody.OS, runtime.GOOS)
  69. }
  70. }
  71. func TestGetEntryIDsHandlerRequiresAuthentication(t *testing.T) {
  72. handler := NewHandler(nil, nil)
  73. r := httptest.NewRequest(http.MethodGet, "/v1/entries/ids", nil)
  74. w := httptest.NewRecorder()
  75. handler.ServeHTTP(w, r)
  76. if got := w.Code; got != http.StatusUnauthorized {
  77. t.Fatalf(`Unexpected status code, got %d instead of %d`, got, http.StatusUnauthorized)
  78. }
  79. }
  80. func TestGetEntryIDsHandlerRejectsInvalidStarredParam(t *testing.T) {
  81. handler := NewHandler(nil, nil)
  82. r := httptest.NewRequest(http.MethodGet, "/v1/entries/ids?starred=maybe", nil)
  83. w := httptest.NewRecorder()
  84. handler.ServeHTTP(w, r)
  85. // Unauthenticated request should be rejected before param validation.
  86. if got := w.Code; got != http.StatusUnauthorized {
  87. t.Fatalf(`Unexpected status code, got %d instead of %d`, got, http.StatusUnauthorized)
  88. }
  89. }
  90. func TestGetEntryIDsHandlerRejectsInvalidStatusParam(t *testing.T) {
  91. handler := NewHandler(nil, nil)
  92. r := httptest.NewRequest(http.MethodGet, "/v1/entries/ids?status=invalid", nil)
  93. w := httptest.NewRecorder()
  94. handler.ServeHTTP(w, r)
  95. // Unauthenticated request should be rejected before param validation.
  96. if got := w.Code; got != http.StatusUnauthorized {
  97. t.Fatalf(`Unexpected status code, got %d instead of %d`, got, http.StatusUnauthorized)
  98. }
  99. }
  100. func TestParseEntryIDsParamsDefaults(t *testing.T) {
  101. r := httptest.NewRequest(http.MethodGet, "/v1/entries/ids", nil)
  102. limit, offset := parseEntryIDsParams(r)
  103. if limit != 10000 {
  104. t.Fatalf(`Expected default limit 10000, got %d`, limit)
  105. }
  106. if offset != 0 {
  107. t.Fatalf(`Expected default offset 0, got %d`, offset)
  108. }
  109. }
  110. func TestParseEntryIDsParamsCustomValues(t *testing.T) {
  111. r := httptest.NewRequest(http.MethodGet, "/v1/entries/ids?limit=500&offset=100", nil)
  112. limit, offset := parseEntryIDsParams(r)
  113. if limit != 500 {
  114. t.Fatalf(`Expected limit 500, got %d`, limit)
  115. }
  116. if offset != 100 {
  117. t.Fatalf(`Expected offset 100, got %d`, offset)
  118. }
  119. }
  120. func TestParseEntryIDsParamsLimitCappedAtMaximum(t *testing.T) {
  121. r := httptest.NewRequest(http.MethodGet, "/v1/entries/ids?limit=99999", nil)
  122. limit, _ := parseEntryIDsParams(r)
  123. if limit != 10000 {
  124. t.Fatalf(`Expected limit capped at 10000, got %d`, limit)
  125. }
  126. }
  127. func TestParseEntryIDsParamsZeroLimitUsesDefault(t *testing.T) {
  128. r := httptest.NewRequest(http.MethodGet, "/v1/entries/ids?limit=0", nil)
  129. limit, _ := parseEntryIDsParams(r)
  130. if limit != 10000 {
  131. t.Fatalf(`Expected zero limit to use default 10000, got %d`, limit)
  132. }
  133. }
  134. func TestNewHandlerSupportsBasePathStripping(t *testing.T) {
  135. scenarios := []struct {
  136. name string
  137. prefix string
  138. path string
  139. }{
  140. {name: "empty base path", prefix: "", path: "/v1/users"},
  141. {name: "non empty base path", prefix: "/base", path: "/base/v1/users"},
  142. }
  143. for _, scenario := range scenarios {
  144. t.Run(scenario.name, func(t *testing.T) {
  145. handler := http.StripPrefix(scenario.prefix, NewHandler(nil, nil))
  146. r := httptest.NewRequest(http.MethodOptions, scenario.path, nil)
  147. w := httptest.NewRecorder()
  148. handler.ServeHTTP(w, r)
  149. if got := w.Code; got != http.StatusNoContent {
  150. t.Fatalf(`Unexpected status code, got %d instead of %d`, got, http.StatusNoContent)
  151. }
  152. })
  153. }
  154. }
  155. func TestBasicAuthIsRejectedWhenLocalAuthDisabled(t *testing.T) {
  156. t.Setenv("DISABLE_LOCAL_AUTH", "1")
  157. t.Setenv("OAUTH2_PROVIDER", "oidc")
  158. t.Setenv("OAUTH2_CLIENT_ID", "client")
  159. t.Setenv("OAUTH2_CLIENT_SECRET", "secret")
  160. t.Setenv("OAUTH2_REDIRECT_URL", "https://example.org/oauth2/oidc/callback")
  161. t.Setenv("OAUTH2_OIDC_DISCOVERY_ENDPOINT", "https://example.org")
  162. parsedOptions, err := config.NewConfigParser().ParseEnvironmentVariables()
  163. if err != nil {
  164. t.Fatalf("Unable to configure test options: %v", err)
  165. }
  166. previousOptions := config.Opts
  167. config.Opts = parsedOptions
  168. t.Cleanup(func() {
  169. config.Opts = previousOptions
  170. })
  171. handler := NewHandler(nil, nil)
  172. r := httptest.NewRequest(http.MethodGet, "/v1/me", nil)
  173. r.SetBasicAuth("admin", "password")
  174. w := httptest.NewRecorder()
  175. handler.ServeHTTP(w, r)
  176. if got := w.Code; got != http.StatusUnauthorized {
  177. t.Fatalf(`Unexpected status code, got %d instead of %d`, got, http.StatusUnauthorized)
  178. }
  179. }