StoreFile now uses a per-hash sync.Mutex to prevent race conditions when multiple workers (launcher fetcher or parallel uploads) write the same file simultaneously. Duplicate writes are idempotent — if another goroutine stored the file while we waited, return the existing hash without re-writing.
175 lines
4.3 KiB
Go
175 lines
4.3 KiB
Go
package cas
|
|
|
|
import (
|
|
"crypto/sha1"
|
|
"encoding/hex"
|
|
"os"
|
|
"path/filepath"
|
|
"sync"
|
|
"sync/atomic"
|
|
"testing"
|
|
)
|
|
|
|
func TestIsValidHash(t *testing.T) {
|
|
tests := []struct {
|
|
hash string
|
|
want bool
|
|
}{
|
|
{"a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2", true},
|
|
{"0000000000000000000000000000000000000000", true},
|
|
{"ffffffffffffffffffffffffffffffffffffffff", true},
|
|
{"A1B2C3D4E5F6A1B2C3D4E5F6A1B2C3D4E5F6A1B2", false}, // uppercase
|
|
{"g1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2", false}, // non-hex
|
|
{"a1b2c3d4e5f6", false}, // too short
|
|
{"", false}, // empty
|
|
{"a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3", false}, // too long (41)
|
|
}
|
|
for _, tt := range tests {
|
|
got := isValidHash(tt.hash)
|
|
if got != tt.want {
|
|
t.Errorf("isValidHash(%q) = %v, want %v", tt.hash, got, tt.want)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestStoreFile(t *testing.T) {
|
|
dir := t.TempDir()
|
|
data := []byte("hello minecraft world")
|
|
|
|
hash, err := StoreFile(dir, data)
|
|
if err != nil {
|
|
t.Fatalf("StoreFile failed: %v", err)
|
|
}
|
|
if len(hash) != 40 {
|
|
t.Errorf("expected 40-char hash, got %d", len(hash))
|
|
}
|
|
|
|
// File should exist at dir/<prefix>/<hash>.
|
|
path := filepath.Join(dir, hash[:2], hash)
|
|
info, err := os.Stat(path)
|
|
if err != nil {
|
|
t.Fatalf("stored file not found: %v", err)
|
|
}
|
|
if info.Size() != int64(len(data)) {
|
|
t.Errorf("stored file size = %d, want %d", info.Size(), len(data))
|
|
}
|
|
}
|
|
|
|
func TestStoreFile_Duplicate(t *testing.T) {
|
|
dir := t.TempDir()
|
|
data := []byte("same content")
|
|
|
|
h1, err := StoreFile(dir, data)
|
|
if err != nil {
|
|
t.Fatalf("first StoreFile failed: %v", err)
|
|
}
|
|
h2, err := StoreFile(dir, data)
|
|
if err != nil {
|
|
t.Fatalf("second StoreFile failed: %v", err)
|
|
}
|
|
if h1 != h2 {
|
|
t.Errorf("same data produced different hashes: %s vs %s", h1, h2)
|
|
}
|
|
}
|
|
|
|
func TestStoreFile_ConcurrentSameHash(t *testing.T) {
|
|
dir := t.TempDir()
|
|
data := []byte("concurrent write test")
|
|
|
|
const workers = 10
|
|
var success int64
|
|
var wg sync.WaitGroup
|
|
wg.Add(workers)
|
|
|
|
for i := 0; i < workers; i++ {
|
|
go func() {
|
|
defer wg.Done()
|
|
hash, err := StoreFile(dir, data)
|
|
if err != nil {
|
|
t.Errorf("StoreFile failed: %v", err)
|
|
return
|
|
}
|
|
if len(hash) != 40 {
|
|
t.Errorf("invalid hash length: %d", len(hash))
|
|
return
|
|
}
|
|
atomic.AddInt64(&success, 1)
|
|
}()
|
|
}
|
|
|
|
wg.Wait()
|
|
|
|
if success != workers {
|
|
t.Errorf("expected %d successes, got %d", workers, success)
|
|
}
|
|
|
|
// All goroutines must produce the same hash for identical data.
|
|
h := sha1.Sum(data)
|
|
hash := hex.EncodeToString(h[:])
|
|
if !FileExists(dir, hash) {
|
|
t.Error("file not found after concurrent writes")
|
|
}
|
|
}
|
|
|
|
func TestFileExists(t *testing.T) {
|
|
dir := t.TempDir()
|
|
data := []byte("test data")
|
|
|
|
hash, _ := StoreFile(dir, data)
|
|
if !FileExists(dir, hash) {
|
|
t.Error("FileExists returned false for stored file")
|
|
}
|
|
if FileExists(dir, "0000000000000000000000000000000000000000") {
|
|
t.Error("FileExists returned true for non-existent file")
|
|
}
|
|
}
|
|
|
|
func TestVerifyAndStore(t *testing.T) {
|
|
dir := t.TempDir()
|
|
data := []byte("verify me")
|
|
hash, _ := StoreFile(dir, data)
|
|
|
|
// Correct hash → should succeed (idempotent).
|
|
got, err := VerifyAndStore(dir, data, hash)
|
|
if err != nil {
|
|
t.Errorf("VerifyAndStore with correct hash failed: %v", err)
|
|
}
|
|
if got != hash {
|
|
t.Errorf("hash mismatch: got %s, want %s", got, hash)
|
|
}
|
|
|
|
// Wrong hash → should fail.
|
|
_, err = VerifyAndStore(dir, data, "0000000000000000000000000000000000000000")
|
|
if err == nil {
|
|
t.Error("VerifyAndStore with wrong hash should have failed")
|
|
}
|
|
}
|
|
|
|
func TestDetectContentType(t *testing.T) {
|
|
tests := []struct {
|
|
fileName string
|
|
want string
|
|
}{
|
|
{"mod.jar", "application/java-archive"},
|
|
{"config.json", "application/json"},
|
|
{"skin.png", "image/png"},
|
|
{"pack.zip", "application/zip"},
|
|
{"options.toml", "application/toml"},
|
|
{"server.cfg", "text/plain"},
|
|
{"notes.txt", "text/plain"},
|
|
{"data.xml", "application/xml"},
|
|
{"config.yml", "application/x-yaml"},
|
|
{"config.yaml", "application/x-yaml"},
|
|
{"game.properties", "text/plain"},
|
|
{"unknown.dat", "application/octet-stream"},
|
|
{"noext", "application/octet-stream"},
|
|
{"UPPER.JAR", "application/java-archive"},
|
|
}
|
|
for _, tt := range tests {
|
|
got := detectContentType(tt.fileName)
|
|
if got != tt.want {
|
|
t.Errorf("detectContentType(%q) = %q, want %q", tt.fileName, got, tt.want)
|
|
}
|
|
}
|
|
}
|