Merge branch 'wyh'

Change-Id: If76ca542acb05c805fc6f3d64fb7e8e62d5677e0
diff --git a/src/main/java/com/example/g8backend/util/TorrentUtil.java b/src/main/java/com/example/g8backend/util/TorrentUtil.java
index e6c1f3b..e8360bc 100644
--- a/src/main/java/com/example/g8backend/util/TorrentUtil.java
+++ b/src/main/java/com/example/g8backend/util/TorrentUtil.java
@@ -54,7 +54,7 @@
         }
     }
 
-    private static byte[] readBytes(File file) throws IOException {
+    public static byte[] readBytes(File file) throws IOException {
         try (InputStream in = new FileInputStream(file)) {
             return in.readAllBytes();
         }
diff --git a/src/main/resources/data.sql b/src/main/resources/data.sql
new file mode 100644
index 0000000..0907033
--- /dev/null
+++ b/src/main/resources/data.sql
@@ -0,0 +1 @@
+# 后面统一数据库数据用
\ No newline at end of file
diff --git a/src/main/resources/schema.sql b/src/main/resources/schema.sql
index e88ff07..952313e 100644
--- a/src/main/resources/schema.sql
+++ b/src/main/resources/schema.sql
@@ -41,6 +41,21 @@
     FOREIGN KEY (user_id) REFERENCES users(user_id)
 );
 
+CREATE TABLE IF NOT EXISTS `tags`(
+  tag_id INT AUTO_INCREMENT PRIMARY KEY,
+  tag_name VARCHAR(255) NOT NULL UNIQUE,
+  parent_id INT DEFAULT NULL,
+  FOREIGN KEY (parent_id) REFERENCES tags(tag_id)
+);
+
+CREATE TABLE IF NOT EXISTS `post_tag` (
+    post_id INT NOT NULL,
+    tag_id INT NOT NULL,
+    FOREIGN KEY (post_id) REFERENCES posts(post_id),
+    FOREIGN KEY (tag_id) REFERENCES tags(tag_id),
+    PRIMARY KEY (post_id, tag_id)
+);
+
 CREATE TABLE IF NOT EXISTS `post_likes` (
     user_id INT NOT NULL,
     post_id INT NOT NULL,
diff --git a/src/test/java/com/example/g8backend/service/TorrentServiceTest.java b/src/test/java/com/example/g8backend/service/TorrentServiceTest.java
new file mode 100644
index 0000000..5063501
--- /dev/null
+++ b/src/test/java/com/example/g8backend/service/TorrentServiceTest.java
@@ -0,0 +1,158 @@
+package com.example.g8backend.service;
+
+import com.dampcake.bencode.Bencode;
+import com.example.g8backend.entity.Torrent;
+import com.example.g8backend.mapper.TorrentMapper;
+import com.example.g8backend.service.impl.TorrentServiceImpl;
+import com.example.g8backend.util.TorrentUtil;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.junit.jupiter.api.io.TempDir;
+import org.mockito.InjectMocks;
+import org.mockito.Mock;
+import org.mockito.MockedStatic;
+import org.mockito.Mockito;
+import org.mockito.junit.jupiter.MockitoExtension;
+
+import java.io.File;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.HashMap;
+import java.util.Map;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.mockito.ArgumentMatchers.*;
+import static org.mockito.Mockito.*;
+
+@ExtendWith(MockitoExtension.class)
+public class TorrentServiceTest {
+    @InjectMocks
+    private TorrentServiceImpl torrentService;
+
+    @Mock
+    private TorrentMapper torrentMapper;
+
+    @TempDir
+    Path tempDir;
+
+    private File createTestTorrentFile(Map<String, Object> info) throws Exception {
+        // 1. 构造 Bencode 数据结构
+        Map<String, Object> torrentMap = new HashMap<>();
+        torrentMap.put("announce", "http://localhost:6881/announce");
+        torrentMap.put("info", info);
+
+        // 2. 编码为 Bencode 字节流
+        Bencode bencode = new Bencode();
+        byte[] bencodeData = bencode.encode(torrentMap);
+
+        // 3. 写入临时文件
+        Path torrentPath = tempDir.resolve("dynamic.torrent");
+        Files.write(torrentPath, bencodeData);
+        return torrentPath.toFile();
+    }
+
+    @BeforeEach
+    public void setUp() {
+        torrentService = new TorrentServiceImpl();
+        torrentMapper = Mockito.mock(TorrentMapper.class);
+        // 注入 Mock 对象(反射)
+        try {
+            var field = TorrentServiceImpl.class.getDeclaredField("torrentMapper");
+            field.setAccessible(true);
+            field.set(torrentService, torrentMapper);
+        } catch (Exception e) {
+            throw new RuntimeException(e);
+        }
+    }
+
+    @Test
+    public void testHandleTorrentUpload() throws Exception {
+        Map<String, Object> info = Map.of( "name", "test.txt", "length", 1024);
+        File torrentFile = createTestTorrentFile(info);
+        Long userId = 1L;
+        String passkey = "123456";
+        byte[] mockedBytes = "modified".getBytes();
+        String expectedInfoHash = "modified-info-hash";
+
+        // 注入 Mock 对象(静态方法)
+        try (MockedStatic<TorrentUtil> mockedStatic = Mockito.mockStatic(TorrentUtil.class)){
+            mockedStatic.when(() -> TorrentUtil.injectTracker(any(File.class), any(String.class)))
+                    .thenReturn(mockedBytes);
+            mockedStatic.when(() -> TorrentUtil.getInfoHash(any(File.class)))
+                    .thenReturn(expectedInfoHash);
+            Torrent torrent = torrentService.handleTorrentUpload(torrentFile, userId, passkey);
+
+            // 验证调用
+            verify(torrentMapper, times(1))
+                    .insertTorrent(eq(1L),
+                            anyString(),
+                            eq("modified-info-hash"),
+                            eq(torrentFile.length()/1024.0/1024.0));
+
+            assertEquals(expectedInfoHash, torrent.getInfoHash());
+            assertEquals(userId, torrent.getUserId());
+            assertEquals(torrentFile.length()/1024.0/1024.0, torrent.getFileSize());
+        } finally {
+            if (!torrentFile.delete()) {
+                System.err.println("Failed to delete temporary file: " + torrentFile.getAbsolutePath());
+            }
+        }
+    }
+
+    @Test
+    public void testHandleTorrentDownload() throws Exception {
+        Torrent torrent = new Torrent();
+        torrent.setTorrentName("sample.torrent");
+
+        File fakeTorrentFile = new File("uploaded-torrents/sample.torrent");
+        fakeTorrentFile.getParentFile().mkdirs();
+        byte[] originalBytes = "original-data".getBytes();
+        Files.write(fakeTorrentFile.toPath(), originalBytes);
+
+        String passkey = "testpasskey";
+        byte[] modifiedBytes = "modified-data".getBytes();
+
+        try (MockedStatic<TorrentUtil> mockedStatic = Mockito.mockStatic(TorrentUtil.class)) {
+            // mock injectTracker
+            mockedStatic.when(() -> TorrentUtil.injectTracker(any(File.class), anyString()))
+                    .thenReturn(modifiedBytes);
+
+            File downloaded = torrentService.handleTorrentDownload(torrent, passkey);
+
+            assertTrue(downloaded.exists());
+            assertEquals("modified-data", Files.readString(downloaded.toPath()));
+
+            // 清理临时文件
+            downloaded.delete();
+        } finally {
+            fakeTorrentFile.delete();  // 清理原始文件
+        }
+    }
+
+
+    @Test
+    public void testFindByInfoHash() {
+        Torrent mockTorrent = new Torrent();
+        mockTorrent.setInfoHash("abc123");
+        when(torrentMapper.getTorrentByInfoHash("abc123")).thenReturn(mockTorrent);
+
+        Torrent result = torrentService.findByInfoHash("abc123");
+
+        assertEquals("abc123", result.getInfoHash());
+        verify(torrentMapper).getTorrentByInfoHash("abc123");
+    }
+
+    @Test
+    public void testFindByTorrentId() {
+        Torrent mockTorrent = new Torrent();
+        mockTorrent.setTorrentId(42L);
+        when(torrentMapper.getTorrentByTorrentId(42L)).thenReturn(mockTorrent);
+
+        Torrent result = torrentService.findByTorrentId(42L);
+
+        assertEquals(42L, result.getTorrentId());
+        verify(torrentMapper).getTorrentByTorrentId(42L);
+    }
+}
\ No newline at end of file