보안: - Zip Bomb 방어 (io.LimitReader 100MB) - Redis Del 에러 로깅 (auth, idempotency) - 로그인 실패 로그에서 username 제거 - os.Remove 에러 로깅 모니터링: - Prometheus 메트릭 미들웨어 + /metrics 엔드포인트 - http_requests_total, http_request_duration_seconds 등 4개 메트릭 테스트: - download (11), chain (10), bossraid (20) = 41개 단위 테스트 기타: - DB 모델 GORM 인덱스 태그 추가 - launcherHash 필드 + hashFileToHex() 추가 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
199 lines
4.7 KiB
Go
199 lines
4.7 KiB
Go
package download
|
|
|
|
import (
|
|
"archive/zip"
|
|
"crypto/sha256"
|
|
"encoding/hex"
|
|
"os"
|
|
"path/filepath"
|
|
"testing"
|
|
)
|
|
|
|
func TestHashFileToHex_KnownContent(t *testing.T) {
|
|
dir := t.TempDir()
|
|
path := filepath.Join(dir, "testfile.bin")
|
|
|
|
content := []byte("hello world")
|
|
if err := os.WriteFile(path, content, 0644); err != nil {
|
|
t.Fatalf("failed to write temp file: %v", err)
|
|
}
|
|
|
|
got := hashFileToHex(path)
|
|
|
|
h := sha256.Sum256(content)
|
|
want := hex.EncodeToString(h[:])
|
|
|
|
if got != want {
|
|
t.Errorf("hashFileToHex = %q, want %q", got, want)
|
|
}
|
|
}
|
|
|
|
func TestHashFileToHex_EmptyFile(t *testing.T) {
|
|
dir := t.TempDir()
|
|
path := filepath.Join(dir, "empty.bin")
|
|
|
|
if err := os.WriteFile(path, []byte{}, 0644); err != nil {
|
|
t.Fatalf("failed to write temp file: %v", err)
|
|
}
|
|
|
|
got := hashFileToHex(path)
|
|
|
|
h := sha256.Sum256([]byte{})
|
|
want := hex.EncodeToString(h[:])
|
|
|
|
if got != want {
|
|
t.Errorf("hashFileToHex (empty) = %q, want %q", got, want)
|
|
}
|
|
}
|
|
|
|
func TestHashFileToHex_NonExistentFile(t *testing.T) {
|
|
got := hashFileToHex("/nonexistent/path/file.bin")
|
|
if got != "" {
|
|
t.Errorf("hashFileToHex (nonexistent) = %q, want empty string", got)
|
|
}
|
|
}
|
|
|
|
// createTestZip creates a zip file at zipPath containing the given files.
|
|
// files is a map of filename -> content.
|
|
func createTestZip(t *testing.T, zipPath string, files map[string][]byte) {
|
|
t.Helper()
|
|
f, err := os.Create(zipPath)
|
|
if err != nil {
|
|
t.Fatalf("failed to create zip: %v", err)
|
|
}
|
|
defer f.Close()
|
|
|
|
w := zip.NewWriter(f)
|
|
for name, data := range files {
|
|
fw, err := w.Create(name)
|
|
if err != nil {
|
|
t.Fatalf("failed to create zip entry %s: %v", name, err)
|
|
}
|
|
if _, err := fw.Write(data); err != nil {
|
|
t.Fatalf("failed to write zip entry %s: %v", name, err)
|
|
}
|
|
}
|
|
if err := w.Close(); err != nil {
|
|
t.Fatalf("failed to close zip writer: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestHashGameExeFromZip_WithA301Exe(t *testing.T) {
|
|
dir := t.TempDir()
|
|
zipPath := filepath.Join(dir, "game.zip")
|
|
|
|
exeContent := []byte("fake A301.exe binary content for testing")
|
|
createTestZip(t, zipPath, map[string][]byte{
|
|
"GameFolder/A301.exe": exeContent,
|
|
"GameFolder/readme.txt": []byte("readme"),
|
|
})
|
|
|
|
got := hashGameExeFromZip(zipPath)
|
|
|
|
h := sha256.Sum256(exeContent)
|
|
want := hex.EncodeToString(h[:])
|
|
|
|
if got != want {
|
|
t.Errorf("hashGameExeFromZip = %q, want %q", got, want)
|
|
}
|
|
}
|
|
|
|
func TestHashGameExeFromZip_CaseInsensitive(t *testing.T) {
|
|
dir := t.TempDir()
|
|
zipPath := filepath.Join(dir, "game.zip")
|
|
|
|
exeContent := []byte("case insensitive test")
|
|
createTestZip(t, zipPath, map[string][]byte{
|
|
"build/a301.EXE": exeContent,
|
|
})
|
|
|
|
got := hashGameExeFromZip(zipPath)
|
|
|
|
h := sha256.Sum256(exeContent)
|
|
want := hex.EncodeToString(h[:])
|
|
|
|
if got != want {
|
|
t.Errorf("hashGameExeFromZip (case insensitive) = %q, want %q", got, want)
|
|
}
|
|
}
|
|
|
|
func TestHashGameExeFromZip_NoA301Exe(t *testing.T) {
|
|
dir := t.TempDir()
|
|
zipPath := filepath.Join(dir, "game.zip")
|
|
|
|
createTestZip(t, zipPath, map[string][]byte{
|
|
"GameFolder/other.exe": []byte("not A301"),
|
|
"GameFolder/readme.txt": []byte("readme"),
|
|
})
|
|
|
|
got := hashGameExeFromZip(zipPath)
|
|
if got != "" {
|
|
t.Errorf("hashGameExeFromZip (no A301.exe) = %q, want empty string", got)
|
|
}
|
|
}
|
|
|
|
func TestHashGameExeFromZip_EmptyZip(t *testing.T) {
|
|
dir := t.TempDir()
|
|
zipPath := filepath.Join(dir, "empty.zip")
|
|
|
|
createTestZip(t, zipPath, map[string][]byte{})
|
|
|
|
got := hashGameExeFromZip(zipPath)
|
|
if got != "" {
|
|
t.Errorf("hashGameExeFromZip (empty zip) = %q, want empty string", got)
|
|
}
|
|
}
|
|
|
|
func TestHashGameExeFromZip_InvalidZip(t *testing.T) {
|
|
dir := t.TempDir()
|
|
zipPath := filepath.Join(dir, "notazip.zip")
|
|
|
|
if err := os.WriteFile(zipPath, []byte("this is not a zip file"), 0644); err != nil {
|
|
t.Fatalf("failed to write file: %v", err)
|
|
}
|
|
|
|
got := hashGameExeFromZip(zipPath)
|
|
if got != "" {
|
|
t.Errorf("hashGameExeFromZip (invalid zip) = %q, want empty string", got)
|
|
}
|
|
}
|
|
|
|
func TestVersionRegex(t *testing.T) {
|
|
tests := []struct {
|
|
input string
|
|
want string
|
|
}{
|
|
{"game_v1.2.3.zip", "v1.2.3"},
|
|
{"game_v2.0.zip", "v2.0"},
|
|
{"game_v10.20.30.zip", "v10.20.30"},
|
|
{"game.zip", ""},
|
|
{"noversion", ""},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
got := versionRe.FindString(tt.input)
|
|
if got != tt.want {
|
|
t.Errorf("versionRe.FindString(%q) = %q, want %q", tt.input, got, tt.want)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestGameFilePath(t *testing.T) {
|
|
s := NewService(nil, "/data/game")
|
|
got := s.GameFilePath()
|
|
// filepath.Join normalizes separators per OS
|
|
want := filepath.Join("/data/game", "game.zip")
|
|
if got != want {
|
|
t.Errorf("GameFilePath() = %q, want %q", got, want)
|
|
}
|
|
}
|
|
|
|
func TestLauncherFilePath(t *testing.T) {
|
|
s := NewService(nil, "/data/game")
|
|
got := s.LauncherFilePath()
|
|
want := filepath.Join("/data/game", "launcher.exe")
|
|
if got != want {
|
|
t.Errorf("LauncherFilePath() = %q, want %q", got, want)
|
|
}
|
|
}
|