- cmd/dapp-fm-app: Native desktop app with WebView (Wails) - cmd/dapp-fm: CLI binary for HTTP server mode - pkg/player: Shared player core with Go bindings Architecture: Go decrypts SMSG content, serves via asset handler. Frontend calls Go directly via Wails bindings for manifest/license checks.
464 lines
12 KiB
Go
464 lines
12 KiB
Go
package smsg
|
|
|
|
import (
|
|
"encoding/base64"
|
|
"testing"
|
|
)
|
|
|
|
func TestEncryptDecryptRoundTrip(t *testing.T) {
|
|
msg := NewMessage("Hello, this is a secure message!").
|
|
WithSubject("Test Subject").
|
|
WithFrom("support@example.com")
|
|
|
|
password := "supersecret123"
|
|
|
|
encrypted, err := Encrypt(msg, password)
|
|
if err != nil {
|
|
t.Fatalf("Encrypt failed: %v", err)
|
|
}
|
|
|
|
decrypted, err := Decrypt(encrypted, password)
|
|
if err != nil {
|
|
t.Fatalf("Decrypt failed: %v", err)
|
|
}
|
|
|
|
if decrypted.Body != msg.Body {
|
|
t.Errorf("Body = %q, want %q", decrypted.Body, msg.Body)
|
|
}
|
|
if decrypted.Subject != msg.Subject {
|
|
t.Errorf("Subject = %q, want %q", decrypted.Subject, msg.Subject)
|
|
}
|
|
if decrypted.From != msg.From {
|
|
t.Errorf("From = %q, want %q", decrypted.From, msg.From)
|
|
}
|
|
}
|
|
|
|
func TestBase64RoundTrip(t *testing.T) {
|
|
msg := NewMessage("Base64 test message")
|
|
password := "testpass"
|
|
|
|
encryptedB64, err := EncryptBase64(msg, password)
|
|
if err != nil {
|
|
t.Fatalf("EncryptBase64 failed: %v", err)
|
|
}
|
|
|
|
// Should be valid base64
|
|
if _, err := base64.StdEncoding.DecodeString(encryptedB64); err != nil {
|
|
t.Fatalf("Invalid base64: %v", err)
|
|
}
|
|
|
|
decrypted, err := DecryptBase64(encryptedB64, password)
|
|
if err != nil {
|
|
t.Fatalf("DecryptBase64 failed: %v", err)
|
|
}
|
|
|
|
if decrypted.Body != msg.Body {
|
|
t.Errorf("Body = %q, want %q", decrypted.Body, msg.Body)
|
|
}
|
|
}
|
|
|
|
func TestWithAttachments(t *testing.T) {
|
|
fileContent := base64.StdEncoding.EncodeToString([]byte("Hello, World!"))
|
|
|
|
msg := NewMessage("Please see the attached file.").
|
|
AddAttachment("hello.txt", fileContent, "text/plain").
|
|
AddAttachment("data.json", base64.StdEncoding.EncodeToString([]byte(`{"key":"value"}`)), "application/json")
|
|
|
|
password := "attachtest"
|
|
|
|
encrypted, err := Encrypt(msg, password)
|
|
if err != nil {
|
|
t.Fatalf("Encrypt failed: %v", err)
|
|
}
|
|
|
|
decrypted, err := Decrypt(encrypted, password)
|
|
if err != nil {
|
|
t.Fatalf("Decrypt failed: %v", err)
|
|
}
|
|
|
|
if len(decrypted.Attachments) != 2 {
|
|
t.Fatalf("Attachments count = %d, want 2", len(decrypted.Attachments))
|
|
}
|
|
|
|
att := decrypted.GetAttachment("hello.txt")
|
|
if att == nil {
|
|
t.Fatal("Attachment hello.txt not found")
|
|
}
|
|
if att.Content != fileContent {
|
|
t.Error("Attachment content mismatch")
|
|
}
|
|
if att.MimeType != "text/plain" {
|
|
t.Errorf("MimeType = %q, want %q", att.MimeType, "text/plain")
|
|
}
|
|
}
|
|
|
|
func TestWithReplyKey(t *testing.T) {
|
|
msg := NewMessage("Here's a public key for your reply.").
|
|
WithReplyKey("dGVzdHB1YmxpY2tleWJhc2U2NA==")
|
|
|
|
password := "pki-test"
|
|
|
|
encrypted, err := Encrypt(msg, password)
|
|
if err != nil {
|
|
t.Fatalf("Encrypt failed: %v", err)
|
|
}
|
|
|
|
decrypted, err := Decrypt(encrypted, password)
|
|
if err != nil {
|
|
t.Fatalf("Decrypt failed: %v", err)
|
|
}
|
|
|
|
if decrypted.ReplyKey == nil {
|
|
t.Fatal("ReplyKey is nil")
|
|
}
|
|
if decrypted.ReplyKey.PublicKey != "dGVzdHB1YmxpY2tleWJhc2U2NA==" {
|
|
t.Error("ReplyKey.PublicKey mismatch")
|
|
}
|
|
if decrypted.ReplyKey.Algorithm != "x25519" {
|
|
t.Errorf("Algorithm = %q, want %q", decrypted.ReplyKey.Algorithm, "x25519")
|
|
}
|
|
}
|
|
|
|
func TestWithHint(t *testing.T) {
|
|
msg := NewMessage("Password hint test")
|
|
password := "birthday1990"
|
|
hint := "Your birthday year"
|
|
|
|
encrypted, err := EncryptWithHint(msg, password, hint)
|
|
if err != nil {
|
|
t.Fatalf("EncryptWithHint failed: %v", err)
|
|
}
|
|
|
|
// Get info should include hint
|
|
info, err := GetInfo(encrypted)
|
|
if err != nil {
|
|
t.Fatalf("GetInfo failed: %v", err)
|
|
}
|
|
|
|
if info.Hint != hint {
|
|
t.Errorf("Hint = %q, want %q", info.Hint, hint)
|
|
}
|
|
|
|
// Should still decrypt
|
|
decrypted, err := Decrypt(encrypted, password)
|
|
if err != nil {
|
|
t.Fatalf("Decrypt failed: %v", err)
|
|
}
|
|
|
|
if decrypted.Body != msg.Body {
|
|
t.Error("Body mismatch")
|
|
}
|
|
}
|
|
|
|
func TestWrongPassword(t *testing.T) {
|
|
msg := NewMessage("Secret message")
|
|
password := "correct-password"
|
|
|
|
encrypted, err := Encrypt(msg, password)
|
|
if err != nil {
|
|
t.Fatalf("Encrypt failed: %v", err)
|
|
}
|
|
|
|
_, err = Decrypt(encrypted, "wrong-password")
|
|
if err == nil {
|
|
t.Error("Decrypt with wrong password should have failed")
|
|
}
|
|
}
|
|
|
|
func TestQuickFunctions(t *testing.T) {
|
|
body := "Quick test message"
|
|
password := "quickpass"
|
|
|
|
encrypted, err := QuickEncrypt(body, password)
|
|
if err != nil {
|
|
t.Fatalf("QuickEncrypt failed: %v", err)
|
|
}
|
|
|
|
decrypted, err := QuickDecrypt(encrypted, password)
|
|
if err != nil {
|
|
t.Fatalf("QuickDecrypt failed: %v", err)
|
|
}
|
|
|
|
if decrypted != body {
|
|
t.Errorf("Decrypted = %q, want %q", decrypted, body)
|
|
}
|
|
}
|
|
|
|
func TestUnicodeContent(t *testing.T) {
|
|
msg := NewMessage("日本語メッセージ 🔐 مرحبا").
|
|
WithSubject("Unicode テスト").
|
|
WithFrom("サポート")
|
|
|
|
password := "unicode-test"
|
|
|
|
encrypted, err := Encrypt(msg, password)
|
|
if err != nil {
|
|
t.Fatalf("Encrypt failed: %v", err)
|
|
}
|
|
|
|
decrypted, err := Decrypt(encrypted, password)
|
|
if err != nil {
|
|
t.Fatalf("Decrypt failed: %v", err)
|
|
}
|
|
|
|
if decrypted.Body != msg.Body {
|
|
t.Errorf("Body = %q, want %q", decrypted.Body, msg.Body)
|
|
}
|
|
if decrypted.Subject != msg.Subject {
|
|
t.Errorf("Subject = %q, want %q", decrypted.Subject, msg.Subject)
|
|
}
|
|
}
|
|
|
|
func TestMetadata(t *testing.T) {
|
|
msg := NewMessage("Message with metadata").
|
|
SetMeta("ticket_id", "12345").
|
|
SetMeta("priority", "high")
|
|
|
|
password := "meta-test"
|
|
|
|
encrypted, err := Encrypt(msg, password)
|
|
if err != nil {
|
|
t.Fatalf("Encrypt failed: %v", err)
|
|
}
|
|
|
|
decrypted, err := Decrypt(encrypted, password)
|
|
if err != nil {
|
|
t.Fatalf("Decrypt failed: %v", err)
|
|
}
|
|
|
|
if decrypted.Meta["ticket_id"] != "12345" {
|
|
t.Error("ticket_id metadata mismatch")
|
|
}
|
|
if decrypted.Meta["priority"] != "high" {
|
|
t.Error("priority metadata mismatch")
|
|
}
|
|
}
|
|
|
|
func TestValidate(t *testing.T) {
|
|
msg := NewMessage("Test")
|
|
password := "test"
|
|
|
|
encrypted, _ := Encrypt(msg, password)
|
|
|
|
// Valid SMSG should pass
|
|
if err := Validate(encrypted); err != nil {
|
|
t.Errorf("Validate failed for valid SMSG: %v", err)
|
|
}
|
|
|
|
// Invalid data should fail
|
|
if err := Validate([]byte("not an smsg")); err == nil {
|
|
t.Error("Validate should fail for invalid data")
|
|
}
|
|
}
|
|
|
|
func TestEmptyPasswordError(t *testing.T) {
|
|
msg := NewMessage("Test")
|
|
|
|
_, err := Encrypt(msg, "")
|
|
if err != ErrPasswordRequired {
|
|
t.Errorf("Expected ErrPasswordRequired, got %v", err)
|
|
}
|
|
}
|
|
|
|
func TestEmptyMessageError(t *testing.T) {
|
|
msg := &Message{}
|
|
|
|
_, err := Encrypt(msg, "password")
|
|
if err != ErrEmptyMessage {
|
|
t.Errorf("Expected ErrEmptyMessage, got %v", err)
|
|
}
|
|
}
|
|
|
|
func TestEncryptWithManifest(t *testing.T) {
|
|
msg := NewMessage("Licensed content")
|
|
password := "license-token-123"
|
|
|
|
// Create manifest with tracks
|
|
manifest := NewManifest("Summer EP 2024").
|
|
AddTrackFull("Intro", 0, 30, "intro").
|
|
AddTrackFull("Main Track", 30, 180, "full").
|
|
AddTrack("Outro", 180)
|
|
manifest.Artist = "Test Artist"
|
|
manifest.ReleaseType = "ep"
|
|
manifest.Format = "dapp.fm/v1"
|
|
|
|
encrypted, err := EncryptWithManifest(msg, password, manifest)
|
|
if err != nil {
|
|
t.Fatalf("EncryptWithManifest failed: %v", err)
|
|
}
|
|
|
|
// Get info without decryption - should have manifest
|
|
header, err := GetInfo(encrypted)
|
|
if err != nil {
|
|
t.Fatalf("GetInfo failed: %v", err)
|
|
}
|
|
|
|
if header.Manifest == nil {
|
|
t.Fatal("Expected manifest in header")
|
|
}
|
|
|
|
if header.Manifest.Title != "Summer EP 2024" {
|
|
t.Errorf("Title = %q, want %q", header.Manifest.Title, "Summer EP 2024")
|
|
}
|
|
|
|
if header.Manifest.Artist != "Test Artist" {
|
|
t.Errorf("Artist = %q, want %q", header.Manifest.Artist, "Test Artist")
|
|
}
|
|
|
|
if header.Manifest.ReleaseType != "ep" {
|
|
t.Errorf("ReleaseType = %q, want %q", header.Manifest.ReleaseType, "ep")
|
|
}
|
|
|
|
if len(header.Manifest.Tracks) != 3 {
|
|
t.Errorf("Tracks count = %d, want 3", len(header.Manifest.Tracks))
|
|
}
|
|
|
|
// Verify tracks
|
|
if header.Manifest.Tracks[0].Title != "Intro" {
|
|
t.Errorf("Track 0 Title = %q, want %q", header.Manifest.Tracks[0].Title, "Intro")
|
|
}
|
|
if header.Manifest.Tracks[0].Start != 0 {
|
|
t.Errorf("Track 0 Start = %v, want 0", header.Manifest.Tracks[0].Start)
|
|
}
|
|
if header.Manifest.Tracks[0].Type != "intro" {
|
|
t.Errorf("Track 0 Type = %q, want %q", header.Manifest.Tracks[0].Type, "intro")
|
|
}
|
|
|
|
// Can still decrypt normally
|
|
decrypted, err := Decrypt(encrypted, password)
|
|
if err != nil {
|
|
t.Fatalf("Decrypt failed: %v", err)
|
|
}
|
|
|
|
if decrypted.Body != "Licensed content" {
|
|
t.Errorf("Body = %q, want %q", decrypted.Body, "Licensed content")
|
|
}
|
|
}
|
|
|
|
func TestManifestBuilder(t *testing.T) {
|
|
manifest := NewManifest("Test Album")
|
|
manifest.Artist = "Artist Name"
|
|
manifest.Album = "Album Name"
|
|
manifest.Year = 2024
|
|
manifest.Genre = "Electronic"
|
|
manifest.ReleaseType = "album"
|
|
manifest.Tags = []string{"electronic", "ambient"}
|
|
manifest.Extra["custom_field"] = "custom_value"
|
|
|
|
// Add tracks
|
|
manifest.AddTrack("Track 1", 0)
|
|
manifest.AddTrack("Track 2", 120)
|
|
manifest.AddTrackFull("Track 3", 240, 360, "outro")
|
|
|
|
if manifest.Title != "Test Album" {
|
|
t.Errorf("Title = %q, want %q", manifest.Title, "Test Album")
|
|
}
|
|
|
|
if len(manifest.Tracks) != 3 {
|
|
t.Fatalf("Track count = %d, want 3", len(manifest.Tracks))
|
|
}
|
|
|
|
// First track should have TrackNum 1
|
|
if manifest.Tracks[0].TrackNum != 1 {
|
|
t.Errorf("Track 1 TrackNum = %d, want 1", manifest.Tracks[0].TrackNum)
|
|
}
|
|
|
|
// Third track should have end time
|
|
if manifest.Tracks[2].End != 360 {
|
|
t.Errorf("Track 3 End = %v, want 360", manifest.Tracks[2].End)
|
|
}
|
|
}
|
|
|
|
func TestManifestExpiration(t *testing.T) {
|
|
// Test perpetual license (no expiration)
|
|
perpetual := NewManifest("Perpetual Album")
|
|
if perpetual.IsExpired() {
|
|
t.Error("Perpetual license should not be expired")
|
|
}
|
|
if perpetual.TimeRemaining() != 0 {
|
|
t.Error("Perpetual license should have 0 time remaining (infinite)")
|
|
}
|
|
if perpetual.LicenseType != "perpetual" {
|
|
t.Errorf("LicenseType = %q, want perpetual", perpetual.LicenseType)
|
|
}
|
|
|
|
// Test streaming access (24 hours)
|
|
stream := NewManifest("Stream Album").WithStreamingAccess(24)
|
|
if stream.IsExpired() {
|
|
t.Error("Streaming license should not be expired immediately")
|
|
}
|
|
if stream.LicenseType != "stream" {
|
|
t.Errorf("LicenseType = %q, want stream", stream.LicenseType)
|
|
}
|
|
remaining := stream.TimeRemaining()
|
|
if remaining < 86000 || remaining > 86400 {
|
|
t.Errorf("TimeRemaining = %d, expected ~86400", remaining)
|
|
}
|
|
|
|
// Test rental with duration
|
|
rental := NewManifest("Rental Album").WithRentalDuration(3600) // 1 hour
|
|
if rental.IsExpired() {
|
|
t.Error("Rental license should not be expired immediately")
|
|
}
|
|
if rental.LicenseType != "rental" {
|
|
t.Errorf("LicenseType = %q, want rental", rental.LicenseType)
|
|
}
|
|
|
|
// Test preview (30 seconds)
|
|
preview := NewManifest("Preview Track").WithPreviewAccess(30)
|
|
if preview.IsExpired() {
|
|
t.Error("Preview license should not be expired immediately")
|
|
}
|
|
if preview.LicenseType != "preview" {
|
|
t.Errorf("LicenseType = %q, want preview", preview.LicenseType)
|
|
}
|
|
|
|
// Test already expired license
|
|
expired := NewManifest("Expired Album")
|
|
expired.ExpiresAt = 1000 // Very old timestamp
|
|
if !expired.IsExpired() {
|
|
t.Error("License with old expiration should be expired")
|
|
}
|
|
if expired.TimeRemaining() >= 0 {
|
|
t.Error("Expired license should have negative time remaining")
|
|
}
|
|
}
|
|
|
|
func TestExpirationInHeader(t *testing.T) {
|
|
msg := NewMessage("Licensed content")
|
|
password := "stream-token-123"
|
|
|
|
// Create streaming license (24 hours)
|
|
manifest := NewManifest("Streaming EP").WithStreamingAccess(24)
|
|
|
|
encrypted, err := EncryptWithManifest(msg, password, manifest)
|
|
if err != nil {
|
|
t.Fatalf("EncryptWithManifest failed: %v", err)
|
|
}
|
|
|
|
// Get info should show expiration
|
|
header, err := GetInfo(encrypted)
|
|
if err != nil {
|
|
t.Fatalf("GetInfo failed: %v", err)
|
|
}
|
|
|
|
if header.Manifest == nil {
|
|
t.Fatal("Expected manifest in header")
|
|
}
|
|
|
|
if header.Manifest.LicenseType != "stream" {
|
|
t.Errorf("LicenseType = %q, want stream", header.Manifest.LicenseType)
|
|
}
|
|
|
|
if header.Manifest.ExpiresAt == 0 {
|
|
t.Error("ExpiresAt should not be 0 for streaming license")
|
|
}
|
|
|
|
if header.Manifest.IssuedAt == 0 {
|
|
t.Error("IssuedAt should not be 0")
|
|
}
|
|
|
|
if header.Manifest.IsExpired() {
|
|
t.Error("New streaming license should not be expired")
|
|
}
|
|
}
|