custom_geo_test.go 8.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330
  1. package service
  2. import (
  3. "errors"
  4. "fmt"
  5. "net/http"
  6. "net/http/httptest"
  7. "os"
  8. "path/filepath"
  9. "testing"
  10. "github.com/mhsanaei/3x-ui/v2/database/model"
  11. )
  12. func TestNormalizeAliasKey(t *testing.T) {
  13. if got := NormalizeAliasKey("GeoIP-IR"); got != "geoip_ir" {
  14. t.Fatalf("got %q", got)
  15. }
  16. if got := NormalizeAliasKey("a-b_c"); got != "a_b_c" {
  17. t.Fatalf("got %q", got)
  18. }
  19. }
  20. func TestNewCustomGeoService(t *testing.T) {
  21. s := NewCustomGeoService()
  22. if err := s.validateAlias("ok_alias-1"); err != nil {
  23. t.Fatal(err)
  24. }
  25. }
  26. func TestTriggerUpdateAllAllSuccess(t *testing.T) {
  27. s := CustomGeoService{}
  28. s.updateAllGetAll = func() ([]model.CustomGeoResource, error) {
  29. return []model.CustomGeoResource{
  30. {Id: 1, Alias: "a"},
  31. {Id: 2, Alias: "b"},
  32. }, nil
  33. }
  34. s.updateAllApply = func(id int, onStartup bool) (string, error) {
  35. return fmt.Sprintf("geo_%d.dat", id), nil
  36. }
  37. restartCalls := 0
  38. s.updateAllRestart = func() error {
  39. restartCalls++
  40. return nil
  41. }
  42. res, err := s.TriggerUpdateAll()
  43. if err != nil {
  44. t.Fatal(err)
  45. }
  46. if len(res.Succeeded) != 2 || len(res.Failed) != 0 {
  47. t.Fatalf("unexpected result: %+v", res)
  48. }
  49. if restartCalls != 1 {
  50. t.Fatalf("expected 1 restart, got %d", restartCalls)
  51. }
  52. }
  53. func TestTriggerUpdateAllPartialSuccess(t *testing.T) {
  54. s := CustomGeoService{}
  55. s.updateAllGetAll = func() ([]model.CustomGeoResource, error) {
  56. return []model.CustomGeoResource{
  57. {Id: 1, Alias: "ok"},
  58. {Id: 2, Alias: "bad"},
  59. }, nil
  60. }
  61. s.updateAllApply = func(id int, onStartup bool) (string, error) {
  62. if id == 2 {
  63. return "geo_2.dat", ErrCustomGeoDownload
  64. }
  65. return "geo_1.dat", nil
  66. }
  67. restartCalls := 0
  68. s.updateAllRestart = func() error {
  69. restartCalls++
  70. return nil
  71. }
  72. res, err := s.TriggerUpdateAll()
  73. if err != nil {
  74. t.Fatal(err)
  75. }
  76. if len(res.Succeeded) != 1 || len(res.Failed) != 1 {
  77. t.Fatalf("unexpected result: %+v", res)
  78. }
  79. if restartCalls != 1 {
  80. t.Fatalf("expected 1 restart, got %d", restartCalls)
  81. }
  82. }
  83. func TestTriggerUpdateAllAllFailure(t *testing.T) {
  84. s := CustomGeoService{}
  85. s.updateAllGetAll = func() ([]model.CustomGeoResource, error) {
  86. return []model.CustomGeoResource{
  87. {Id: 1, Alias: "a"},
  88. {Id: 2, Alias: "b"},
  89. }, nil
  90. }
  91. s.updateAllApply = func(id int, onStartup bool) (string, error) {
  92. return fmt.Sprintf("geo_%d.dat", id), ErrCustomGeoDownload
  93. }
  94. restartCalls := 0
  95. s.updateAllRestart = func() error {
  96. restartCalls++
  97. return nil
  98. }
  99. res, err := s.TriggerUpdateAll()
  100. if err != nil {
  101. t.Fatal(err)
  102. }
  103. if len(res.Succeeded) != 0 || len(res.Failed) != 2 {
  104. t.Fatalf("unexpected result: %+v", res)
  105. }
  106. if restartCalls != 0 {
  107. t.Fatalf("expected 0 restart, got %d", restartCalls)
  108. }
  109. }
  110. func TestCustomGeoValidateAlias(t *testing.T) {
  111. s := CustomGeoService{}
  112. if err := s.validateAlias(""); !errors.Is(err, ErrCustomGeoAliasRequired) {
  113. t.Fatal("empty alias")
  114. }
  115. if err := s.validateAlias("Bad"); !errors.Is(err, ErrCustomGeoAliasPattern) {
  116. t.Fatal("uppercase")
  117. }
  118. if err := s.validateAlias("a b"); !errors.Is(err, ErrCustomGeoAliasPattern) {
  119. t.Fatal("space")
  120. }
  121. if err := s.validateAlias("ok_alias-1"); err != nil {
  122. t.Fatal(err)
  123. }
  124. if err := s.validateAlias("geoip"); !errors.Is(err, ErrCustomGeoAliasReserved) {
  125. t.Fatal("reserved")
  126. }
  127. }
  128. func TestCustomGeoValidateURL(t *testing.T) {
  129. s := CustomGeoService{}
  130. if err := s.validateURL(""); !errors.Is(err, ErrCustomGeoURLRequired) {
  131. t.Fatal("empty")
  132. }
  133. if err := s.validateURL("ftp://x"); !errors.Is(err, ErrCustomGeoURLScheme) {
  134. t.Fatal("ftp")
  135. }
  136. if err := s.validateURL("https://example.com/a.dat"); err != nil {
  137. t.Fatal(err)
  138. }
  139. }
  140. func TestCustomGeoValidateType(t *testing.T) {
  141. s := CustomGeoService{}
  142. if err := s.validateType("geosite"); err != nil {
  143. t.Fatal(err)
  144. }
  145. if err := s.validateType("x"); !errors.Is(err, ErrCustomGeoInvalidType) {
  146. t.Fatal("bad type")
  147. }
  148. }
  149. func TestCustomGeoDownloadToPath(t *testing.T) {
  150. ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
  151. w.Header().Set("X-Test", "1")
  152. if r.Header.Get("If-Modified-Since") != "" {
  153. w.WriteHeader(http.StatusNotModified)
  154. return
  155. }
  156. w.WriteHeader(http.StatusOK)
  157. _, _ = w.Write(make([]byte, minDatBytes+1))
  158. }))
  159. defer ts.Close()
  160. dir := t.TempDir()
  161. t.Setenv("XUI_BIN_FOLDER", dir)
  162. dest := filepath.Join(dir, "geoip_t.dat")
  163. s := CustomGeoService{}
  164. skipped, _, err := s.downloadToPath(ts.URL, dest, "")
  165. if err != nil {
  166. t.Fatal(err)
  167. }
  168. if skipped {
  169. t.Fatal("expected download")
  170. }
  171. st, err := os.Stat(dest)
  172. if err != nil || st.Size() < minDatBytes {
  173. t.Fatalf("file %v", err)
  174. }
  175. skipped2, _, err2 := s.downloadToPath(ts.URL, dest, "")
  176. if err2 != nil || !skipped2 {
  177. t.Fatalf("304 expected skipped=%v err=%v", skipped2, err2)
  178. }
  179. }
  180. func TestCustomGeoDownloadToPath_missingLocalSendsNoIMSFromDB(t *testing.T) {
  181. lm := "Wed, 21 Oct 2015 07:28:00 GMT"
  182. ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
  183. if r.Header.Get("If-Modified-Since") != "" {
  184. w.WriteHeader(http.StatusNotModified)
  185. return
  186. }
  187. w.Header().Set("Last-Modified", lm)
  188. w.WriteHeader(http.StatusOK)
  189. _, _ = w.Write(make([]byte, minDatBytes+1))
  190. }))
  191. defer ts.Close()
  192. dir := t.TempDir()
  193. t.Setenv("XUI_BIN_FOLDER", dir)
  194. dest := filepath.Join(dir, "geoip_rebuild.dat")
  195. s := CustomGeoService{}
  196. skipped, _, err := s.downloadToPath(ts.URL, dest, lm)
  197. if err != nil {
  198. t.Fatal(err)
  199. }
  200. if skipped {
  201. t.Fatal("must not treat as not-modified when local file is missing")
  202. }
  203. if _, err := os.Stat(dest); err != nil {
  204. t.Fatal("file should exist after container-style rebuild")
  205. }
  206. }
  207. func TestCustomGeoDownloadToPath_repairSkipsConditional(t *testing.T) {
  208. lm := "Wed, 21 Oct 2015 07:28:00 GMT"
  209. ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
  210. if r.Header.Get("If-Modified-Since") != "" {
  211. w.WriteHeader(http.StatusNotModified)
  212. return
  213. }
  214. w.Header().Set("Last-Modified", lm)
  215. w.WriteHeader(http.StatusOK)
  216. _, _ = w.Write(make([]byte, minDatBytes+1))
  217. }))
  218. defer ts.Close()
  219. dir := t.TempDir()
  220. t.Setenv("XUI_BIN_FOLDER", dir)
  221. dest := filepath.Join(dir, "geoip_bad.dat")
  222. if err := os.WriteFile(dest, make([]byte, minDatBytes-1), 0o644); err != nil {
  223. t.Fatal(err)
  224. }
  225. s := CustomGeoService{}
  226. skipped, _, err := s.downloadToPath(ts.URL, dest, lm)
  227. if err != nil {
  228. t.Fatal(err)
  229. }
  230. if skipped {
  231. t.Fatal("corrupt local file must be re-downloaded, not 304")
  232. }
  233. st, err := os.Stat(dest)
  234. if err != nil || st.Size() < minDatBytes {
  235. t.Fatalf("file repaired: %v", err)
  236. }
  237. }
  238. func TestCustomGeoFileNameFor(t *testing.T) {
  239. s := CustomGeoService{}
  240. if s.fileNameFor("geoip", "a") != "geoip_a.dat" {
  241. t.Fatal("geoip name")
  242. }
  243. if s.fileNameFor("geosite", "b") != "geosite_b.dat" {
  244. t.Fatal("geosite name")
  245. }
  246. }
  247. func TestLocalDatFileNeedsRepair(t *testing.T) {
  248. dir := t.TempDir()
  249. if !localDatFileNeedsRepair(filepath.Join(dir, "missing.dat")) {
  250. t.Fatal("missing")
  251. }
  252. smallPath := filepath.Join(dir, "small.dat")
  253. if err := os.WriteFile(smallPath, make([]byte, minDatBytes-1), 0o644); err != nil {
  254. t.Fatal(err)
  255. }
  256. if !localDatFileNeedsRepair(smallPath) {
  257. t.Fatal("small")
  258. }
  259. okPath := filepath.Join(dir, "ok.dat")
  260. if err := os.WriteFile(okPath, make([]byte, minDatBytes), 0o644); err != nil {
  261. t.Fatal(err)
  262. }
  263. if localDatFileNeedsRepair(okPath) {
  264. t.Fatal("ok size")
  265. }
  266. dirPath := filepath.Join(dir, "isdir.dat")
  267. if err := os.Mkdir(dirPath, 0o755); err != nil {
  268. t.Fatal(err)
  269. }
  270. if !localDatFileNeedsRepair(dirPath) {
  271. t.Fatal("dir should need repair")
  272. }
  273. if !CustomGeoLocalFileNeedsRepair(dirPath) {
  274. t.Fatal("exported wrapper dir")
  275. }
  276. if CustomGeoLocalFileNeedsRepair(okPath) {
  277. t.Fatal("exported wrapper ok file")
  278. }
  279. }
  280. func TestProbeCustomGeoURL_HEADOK(t *testing.T) {
  281. ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
  282. if r.Method == http.MethodHead {
  283. w.WriteHeader(http.StatusOK)
  284. return
  285. }
  286. w.WriteHeader(http.StatusOK)
  287. }))
  288. defer ts.Close()
  289. if err := probeCustomGeoURL(ts.URL); err != nil {
  290. t.Fatal(err)
  291. }
  292. }
  293. func TestProbeCustomGeoURL_HEAD405GETRange(t *testing.T) {
  294. ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
  295. if r.Method == http.MethodHead {
  296. w.WriteHeader(http.StatusMethodNotAllowed)
  297. return
  298. }
  299. if r.Method == http.MethodGet && r.Header.Get("Range") != "" {
  300. w.WriteHeader(http.StatusPartialContent)
  301. _, _ = w.Write([]byte{0})
  302. return
  303. }
  304. w.WriteHeader(http.StatusBadRequest)
  305. }))
  306. defer ts.Close()
  307. if err := probeCustomGeoURL(ts.URL); err != nil {
  308. t.Fatal(err)
  309. }
  310. }