azure_sink_test.go 8.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355
  1. package azuresink
  2. import (
  3. "os"
  4. "testing"
  5. "time"
  6. "github.com/seaweedfs/seaweedfs/weed/pb/filer_pb"
  7. )
  8. // MockConfiguration for testing
  9. type mockConfiguration struct {
  10. values map[string]interface{}
  11. }
  12. func newMockConfiguration() *mockConfiguration {
  13. return &mockConfiguration{
  14. values: make(map[string]interface{}),
  15. }
  16. }
  17. func (m *mockConfiguration) GetString(key string) string {
  18. if v, ok := m.values[key]; ok {
  19. return v.(string)
  20. }
  21. return ""
  22. }
  23. func (m *mockConfiguration) GetBool(key string) bool {
  24. if v, ok := m.values[key]; ok {
  25. return v.(bool)
  26. }
  27. return false
  28. }
  29. func (m *mockConfiguration) GetInt(key string) int {
  30. if v, ok := m.values[key]; ok {
  31. return v.(int)
  32. }
  33. return 0
  34. }
  35. func (m *mockConfiguration) GetInt64(key string) int64 {
  36. if v, ok := m.values[key]; ok {
  37. return v.(int64)
  38. }
  39. return 0
  40. }
  41. func (m *mockConfiguration) GetFloat64(key string) float64 {
  42. if v, ok := m.values[key]; ok {
  43. return v.(float64)
  44. }
  45. return 0.0
  46. }
  47. func (m *mockConfiguration) GetStringSlice(key string) []string {
  48. if v, ok := m.values[key]; ok {
  49. return v.([]string)
  50. }
  51. return nil
  52. }
  53. func (m *mockConfiguration) SetDefault(key string, value interface{}) {
  54. if _, exists := m.values[key]; !exists {
  55. m.values[key] = value
  56. }
  57. }
  58. // Test the AzureSink interface implementation
  59. func TestAzureSinkInterface(t *testing.T) {
  60. sink := &AzureSink{}
  61. if sink.GetName() != "azure" {
  62. t.Errorf("Expected name 'azure', got '%s'", sink.GetName())
  63. }
  64. // Test directory setting
  65. sink.dir = "/test/dir"
  66. if sink.GetSinkToDirectory() != "/test/dir" {
  67. t.Errorf("Expected directory '/test/dir', got '%s'", sink.GetSinkToDirectory())
  68. }
  69. // Test incremental setting
  70. sink.isIncremental = true
  71. if !sink.IsIncremental() {
  72. t.Error("Expected isIncremental to be true")
  73. }
  74. }
  75. // Test Azure sink initialization
  76. func TestAzureSinkInitialization(t *testing.T) {
  77. accountName := os.Getenv("AZURE_STORAGE_ACCOUNT")
  78. accountKey := os.Getenv("AZURE_STORAGE_ACCESS_KEY")
  79. testContainer := os.Getenv("AZURE_TEST_CONTAINER")
  80. if accountName == "" || accountKey == "" {
  81. t.Skip("Skipping Azure sink test: AZURE_STORAGE_ACCOUNT or AZURE_STORAGE_ACCESS_KEY not set")
  82. }
  83. if testContainer == "" {
  84. testContainer = "seaweedfs-test"
  85. }
  86. sink := &AzureSink{}
  87. err := sink.initialize(accountName, accountKey, testContainer, "/test")
  88. if err != nil {
  89. t.Fatalf("Failed to initialize Azure sink: %v", err)
  90. }
  91. if sink.container != testContainer {
  92. t.Errorf("Expected container '%s', got '%s'", testContainer, sink.container)
  93. }
  94. if sink.dir != "/test" {
  95. t.Errorf("Expected dir '/test', got '%s'", sink.dir)
  96. }
  97. if sink.client == nil {
  98. t.Error("Expected client to be initialized")
  99. }
  100. }
  101. // Test configuration-based initialization
  102. func TestAzureSinkInitializeFromConfig(t *testing.T) {
  103. accountName := os.Getenv("AZURE_STORAGE_ACCOUNT")
  104. accountKey := os.Getenv("AZURE_STORAGE_ACCESS_KEY")
  105. testContainer := os.Getenv("AZURE_TEST_CONTAINER")
  106. if accountName == "" || accountKey == "" {
  107. t.Skip("Skipping Azure sink config test: AZURE_STORAGE_ACCOUNT or AZURE_STORAGE_ACCESS_KEY not set")
  108. }
  109. if testContainer == "" {
  110. testContainer = "seaweedfs-test"
  111. }
  112. config := newMockConfiguration()
  113. config.values["azure.account_name"] = accountName
  114. config.values["azure.account_key"] = accountKey
  115. config.values["azure.container"] = testContainer
  116. config.values["azure.directory"] = "/test"
  117. config.values["azure.is_incremental"] = true
  118. sink := &AzureSink{}
  119. err := sink.Initialize(config, "azure.")
  120. if err != nil {
  121. t.Fatalf("Failed to initialize from config: %v", err)
  122. }
  123. if !sink.IsIncremental() {
  124. t.Error("Expected incremental to be true")
  125. }
  126. }
  127. // Test cleanKey function
  128. func TestCleanKey(t *testing.T) {
  129. tests := []struct {
  130. input string
  131. expected string
  132. }{
  133. {"/test/file.txt", "test/file.txt"},
  134. {"test/file.txt", "test/file.txt"},
  135. {"/", ""},
  136. {"", ""},
  137. {"/a/b/c", "a/b/c"},
  138. }
  139. for _, tt := range tests {
  140. t.Run(tt.input, func(t *testing.T) {
  141. result := cleanKey(tt.input)
  142. if result != tt.expected {
  143. t.Errorf("cleanKey(%q) = %q, want %q", tt.input, result, tt.expected)
  144. }
  145. })
  146. }
  147. }
  148. // Test entry operations (requires valid credentials)
  149. func TestAzureSinkEntryOperations(t *testing.T) {
  150. accountName := os.Getenv("AZURE_STORAGE_ACCOUNT")
  151. accountKey := os.Getenv("AZURE_STORAGE_ACCESS_KEY")
  152. testContainer := os.Getenv("AZURE_TEST_CONTAINER")
  153. if accountName == "" || accountKey == "" {
  154. t.Skip("Skipping Azure sink entry test: credentials not set")
  155. }
  156. if testContainer == "" {
  157. testContainer = "seaweedfs-test"
  158. }
  159. sink := &AzureSink{}
  160. err := sink.initialize(accountName, accountKey, testContainer, "/test")
  161. if err != nil {
  162. t.Fatalf("Failed to initialize: %v", err)
  163. }
  164. // Test CreateEntry with directory (should be no-op)
  165. t.Run("CreateDirectory", func(t *testing.T) {
  166. entry := &filer_pb.Entry{
  167. IsDirectory: true,
  168. }
  169. err := sink.CreateEntry("/test/dir", entry, nil)
  170. if err != nil {
  171. t.Errorf("CreateEntry for directory should not error: %v", err)
  172. }
  173. })
  174. // Test CreateEntry with file
  175. testKey := "/test-sink-file-" + time.Now().Format("20060102-150405") + ".txt"
  176. t.Run("CreateFile", func(t *testing.T) {
  177. entry := &filer_pb.Entry{
  178. IsDirectory: false,
  179. Content: []byte("Test content for Azure sink"),
  180. Attributes: &filer_pb.FuseAttributes{
  181. Mtime: time.Now().Unix(),
  182. },
  183. }
  184. err := sink.CreateEntry(testKey, entry, nil)
  185. if err != nil {
  186. t.Fatalf("Failed to create entry: %v", err)
  187. }
  188. })
  189. // Test UpdateEntry
  190. t.Run("UpdateEntry", func(t *testing.T) {
  191. oldEntry := &filer_pb.Entry{
  192. Content: []byte("Old content"),
  193. }
  194. newEntry := &filer_pb.Entry{
  195. Content: []byte("New content for update test"),
  196. Attributes: &filer_pb.FuseAttributes{
  197. Mtime: time.Now().Unix(),
  198. },
  199. }
  200. found, err := sink.UpdateEntry(testKey, oldEntry, "/test", newEntry, false, nil)
  201. if err != nil {
  202. t.Fatalf("Failed to update entry: %v", err)
  203. }
  204. if !found {
  205. t.Error("Expected found to be true")
  206. }
  207. })
  208. // Test DeleteEntry
  209. t.Run("DeleteFile", func(t *testing.T) {
  210. err := sink.DeleteEntry(testKey, false, false, nil)
  211. if err != nil {
  212. t.Fatalf("Failed to delete entry: %v", err)
  213. }
  214. })
  215. // Test DeleteEntry with directory marker
  216. testDirKey := "/test-dir-" + time.Now().Format("20060102-150405")
  217. t.Run("DeleteDirectory", func(t *testing.T) {
  218. // First create a directory marker
  219. entry := &filer_pb.Entry{
  220. IsDirectory: false,
  221. Content: []byte(""),
  222. }
  223. err := sink.CreateEntry(testDirKey+"/", entry, nil)
  224. if err != nil {
  225. t.Logf("Warning: Failed to create directory marker: %v", err)
  226. }
  227. // Then delete it
  228. err = sink.DeleteEntry(testDirKey, true, false, nil)
  229. if err != nil {
  230. t.Logf("Warning: Failed to delete directory: %v", err)
  231. }
  232. })
  233. }
  234. // Test CreateEntry with precondition (IfUnmodifiedSince)
  235. func TestAzureSinkPrecondition(t *testing.T) {
  236. accountName := os.Getenv("AZURE_STORAGE_ACCOUNT")
  237. accountKey := os.Getenv("AZURE_STORAGE_ACCESS_KEY")
  238. testContainer := os.Getenv("AZURE_TEST_CONTAINER")
  239. if accountName == "" || accountKey == "" {
  240. t.Skip("Skipping Azure sink precondition test: credentials not set")
  241. }
  242. if testContainer == "" {
  243. testContainer = "seaweedfs-test"
  244. }
  245. sink := &AzureSink{}
  246. err := sink.initialize(accountName, accountKey, testContainer, "/test")
  247. if err != nil {
  248. t.Fatalf("Failed to initialize: %v", err)
  249. }
  250. testKey := "/test-precondition-" + time.Now().Format("20060102-150405") + ".txt"
  251. // Create initial entry
  252. entry := &filer_pb.Entry{
  253. Content: []byte("Initial content"),
  254. Attributes: &filer_pb.FuseAttributes{
  255. Mtime: time.Now().Unix(),
  256. },
  257. }
  258. err = sink.CreateEntry(testKey, entry, nil)
  259. if err != nil {
  260. t.Fatalf("Failed to create initial entry: %v", err)
  261. }
  262. // Try to create again with old mtime (should be skipped due to precondition)
  263. oldEntry := &filer_pb.Entry{
  264. Content: []byte("Should not overwrite"),
  265. Attributes: &filer_pb.FuseAttributes{
  266. Mtime: time.Now().Add(-1 * time.Hour).Unix(), // Old timestamp
  267. },
  268. }
  269. err = sink.CreateEntry(testKey, oldEntry, nil)
  270. // Should either succeed (skip) or fail with precondition error
  271. if err != nil {
  272. t.Logf("Create with old mtime: %v (expected)", err)
  273. }
  274. // Clean up
  275. sink.DeleteEntry(testKey, false, false, nil)
  276. }
  277. // Benchmark tests
  278. func BenchmarkCleanKey(b *testing.B) {
  279. keys := []string{
  280. "/simple/path.txt",
  281. "no/leading/slash.txt",
  282. "/",
  283. "/complex/path/with/many/segments/file.txt",
  284. }
  285. b.ResetTimer()
  286. for i := 0; i < b.N; i++ {
  287. cleanKey(keys[i%len(keys)])
  288. }
  289. }
  290. // Test error handling with invalid credentials
  291. func TestAzureSinkInvalidCredentials(t *testing.T) {
  292. sink := &AzureSink{}
  293. err := sink.initialize("invalid-account", "aW52YWxpZGtleQ==", "test-container", "/test")
  294. if err != nil {
  295. t.Skip("Invalid credentials correctly rejected at initialization")
  296. }
  297. // If initialization succeeded, operations should fail
  298. entry := &filer_pb.Entry{
  299. Content: []byte("test"),
  300. }
  301. err = sink.CreateEntry("/test.txt", entry, nil)
  302. if err == nil {
  303. t.Log("Expected error with invalid credentials, but got none (might be cached)")
  304. }
  305. }