Initial commit
This commit is contained in:
commit
02a6dabcfb
62 files changed
+15751
No files matched your search
@@ -0,0 +1,126 @@
|
||||
package utils
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"reflect"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// JSONBackend defines the interface for JSON encoding/decoding
|
||||
type JSONBackend interface {
|
||||
Marshal(v any) ([]byte, error)
|
||||
Unmarshal(data []byte, v any) error
|
||||
}
|
||||
|
||||
// DefaultJSONBackend uses the standard library encoding/json
|
||||
type DefaultJSONBackend struct{}
|
||||
|
||||
func (DefaultJSONBackend) Marshal(v any) ([]byte, error) {
|
||||
return json.Marshal(v)
|
||||
}
|
||||
|
||||
func (DefaultJSONBackend) Unmarshal(data []byte, v any) error {
|
||||
return json.Unmarshal(data, v)
|
||||
}
|
||||
|
||||
// GetDefaultJSONBackend returns the default JSON backend
|
||||
func GetDefaultJSONBackend() JSONBackend {
|
||||
return DefaultJSONBackend{}
|
||||
}
|
||||
|
||||
// RawMessage is a raw encoded JSON value
|
||||
type RawMessage = json.RawMessage
|
||||
|
||||
// Marshal is a convenience function
|
||||
func Marshal(v any) ([]byte, error) {
|
||||
return GetDefaultJSONBackend().Marshal(v)
|
||||
}
|
||||
|
||||
// Unmarshal is a convenience function
|
||||
func Unmarshal(data []byte, v any) error {
|
||||
return GetDefaultJSONBackend().Unmarshal(data, v)
|
||||
}
|
||||
|
||||
// ToSnakeCase converts camelCase to snake_case
|
||||
func ToSnakeCase(s string) string {
|
||||
if s == "" {
|
||||
return s
|
||||
}
|
||||
var result strings.Builder
|
||||
for i, r := range s {
|
||||
if r >= 'A' && r <= 'Z' {
|
||||
if i > 0 {
|
||||
result.WriteRune('_')
|
||||
}
|
||||
result.WriteRune(r + 'a' - 'A')
|
||||
} else {
|
||||
result.WriteRune(r)
|
||||
}
|
||||
}
|
||||
return result.String()
|
||||
}
|
||||
|
||||
// ToCamelCase converts snake_case to camelCase
|
||||
func ToCamelCase(s string) string {
|
||||
if s == "" {
|
||||
return s
|
||||
}
|
||||
parts := strings.Split(s, "_")
|
||||
if len(parts) == 1 {
|
||||
return s
|
||||
}
|
||||
var result strings.Builder
|
||||
result.WriteString(parts[0])
|
||||
for _, part := range parts[1:] {
|
||||
if len(part) > 0 {
|
||||
result.WriteString(strings.ToUpper(part[:1]) + part[1:])
|
||||
}
|
||||
}
|
||||
return result.String()
|
||||
}
|
||||
|
||||
// MapKeysToSnakeCase converts all keys in a map to snake_case
|
||||
func MapKeysToSnakeCase(m map[string]any) map[string]any {
|
||||
result := make(map[string]any, len(m))
|
||||
for k, v := range m {
|
||||
result[ToSnakeCase(k)] = v
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// MapKeysToCamelCase converts all keys in a map to camelCase
|
||||
func MapKeysToCamelCase(m map[string]any) map[string]any {
|
||||
result := make(map[string]any, len(m))
|
||||
for k, v := range m {
|
||||
result[ToCamelCase(k)] = v
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// StructToMap converts a struct to a map using json tags
|
||||
func StructToMap(v any, forRequest bool) (map[string]any, error) {
|
||||
data, err := Marshal(v)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var result map[string]any
|
||||
if err := Unmarshal(data, &result); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// GetJSONTag returns the json tag for a struct field
|
||||
func GetJSONTag(t reflect.Type, fieldIndex int) string {
|
||||
if fieldIndex >= t.NumField() {
|
||||
return ""
|
||||
}
|
||||
field := t.Field(fieldIndex)
|
||||
tag := field.Tag.Get("json")
|
||||
if tag == "" || tag == "-" {
|
||||
return ""
|
||||
}
|
||||
// Parse tag options (e.g., "name,omitempty")
|
||||
parts := strings.Split(tag, ",")
|
||||
return parts[0]
|
||||
}
|
||||
@@ -0,0 +1,140 @@
|
||||
package utils
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
)
|
||||
|
||||
// Response represents an API response
|
||||
type Response struct {
|
||||
Result json.RawMessage `json:"result"`
|
||||
Error string `json:"error,omitempty"`
|
||||
ErrorDescription string `json:"errorDescription,omitempty"`
|
||||
}
|
||||
|
||||
// GetResult returns the result as raw JSON
|
||||
func (r *Response) GetResult() json.RawMessage {
|
||||
return r.Result
|
||||
}
|
||||
|
||||
// GetError returns the error message
|
||||
func (r *Response) GetError() string {
|
||||
if r.Error != "" {
|
||||
return r.Error
|
||||
}
|
||||
if r.ErrorDescription != "" {
|
||||
return r.ErrorDescription
|
||||
}
|
||||
return "Unknown error"
|
||||
}
|
||||
|
||||
// HasError returns true if the response contains an error
|
||||
func (r *Response) HasError() bool {
|
||||
return r.Error != "" || r.ErrorDescription != ""
|
||||
}
|
||||
|
||||
// UnmarshalJSON implements custom unmarshaling
|
||||
func (r *Response) UnmarshalJSON(data []byte) error {
|
||||
// First try to unmarshal as a standard response
|
||||
type Alias Response
|
||||
aux := &struct {
|
||||
*Alias
|
||||
}{
|
||||
Alias: (*Alias)(r),
|
||||
}
|
||||
if err := json.Unmarshal(data, aux); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// If result is missing, wrap the whole response in result
|
||||
if r.Result == nil {
|
||||
r.Result = data
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// DeJSON creates a Response from JSON data
|
||||
func DeJSON(data []byte) (*Response, error) {
|
||||
var resp Response
|
||||
if err := json.Unmarshal(data, &resp); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &resp, nil
|
||||
}
|
||||
|
||||
// Error types
|
||||
var (
|
||||
ErrUnauthorized = errors.New("unauthorized")
|
||||
ErrBadRequest = errors.New("bad request")
|
||||
ErrNotFound = errors.New("not found")
|
||||
ErrNetwork = errors.New("network error")
|
||||
ErrTimeout = errors.New("timeout")
|
||||
ErrSchemaMismatch = errors.New("schema mismatch")
|
||||
)
|
||||
|
||||
// YandexMusicError is the base error type
|
||||
type YandexMusicError struct {
|
||||
Message string
|
||||
Err error
|
||||
}
|
||||
|
||||
func (e *YandexMusicError) Error() string {
|
||||
if e.Err != nil {
|
||||
return e.Message + ": " + e.Err.Error()
|
||||
}
|
||||
return e.Message
|
||||
}
|
||||
|
||||
func (e *YandexMusicError) Unwrap() error {
|
||||
return e.Err
|
||||
}
|
||||
|
||||
// NewYandexMusicError creates a new YandexMusicError
|
||||
func NewYandexMusicError(message string, err error) *YandexMusicError {
|
||||
return &YandexMusicError{Message: message, Err: err}
|
||||
}
|
||||
|
||||
// IsUnauthorizedError checks if error is unauthorized
|
||||
func IsUnauthorizedError(err error) bool {
|
||||
var yme *YandexMusicError
|
||||
if errors.As(err, &yme) {
|
||||
return errors.Is(yme.Err, ErrUnauthorized)
|
||||
}
|
||||
return errors.Is(err, ErrUnauthorized)
|
||||
}
|
||||
|
||||
// IsBadRequestError checks if error is bad request
|
||||
func IsBadRequestError(err error) bool {
|
||||
var yme *YandexMusicError
|
||||
if errors.As(err, &yme) {
|
||||
return errors.Is(yme.Err, ErrBadRequest)
|
||||
}
|
||||
return errors.Is(err, ErrBadRequest)
|
||||
}
|
||||
|
||||
// IsNotFoundError checks if error is not found
|
||||
func IsNotFoundError(err error) bool {
|
||||
var yme *YandexMusicError
|
||||
if errors.As(err, &yme) {
|
||||
return errors.Is(yme.Err, ErrNotFound)
|
||||
}
|
||||
return errors.Is(err, ErrNotFound)
|
||||
}
|
||||
|
||||
// IsNetworkError checks if error is network error
|
||||
func IsNetworkError(err error) bool {
|
||||
var yme *YandexMusicError
|
||||
if errors.As(err, &yme) {
|
||||
return errors.Is(yme.Err, ErrNetwork)
|
||||
}
|
||||
return errors.Is(err, ErrNetwork)
|
||||
}
|
||||
|
||||
// IsTimeoutError checks if error is timeout
|
||||
func IsTimeoutError(err error) bool {
|
||||
var yme *YandexMusicError
|
||||
if errors.As(err, &yme) {
|
||||
return errors.Is(yme.Err, ErrTimeout)
|
||||
}
|
||||
return errors.Is(err, ErrTimeout)
|
||||
}
|
||||
@@ -0,0 +1,103 @@
|
||||
package utils
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"strings"
|
||||
"sync"
|
||||
)
|
||||
|
||||
// SchemaMismatch represents a mismatch between expected and actual API response
|
||||
type SchemaMismatch struct {
|
||||
ModelName string
|
||||
MissingFields []string
|
||||
UnknownFields []string
|
||||
UnknownValues map[string]any
|
||||
Endpoint string
|
||||
LibraryVersion string
|
||||
}
|
||||
|
||||
func (sm *SchemaMismatch) Describe() string {
|
||||
var parts []string
|
||||
parts = append(parts, fmt.Sprintf("Model: %s", sm.ModelName))
|
||||
if len(sm.MissingFields) > 0 {
|
||||
parts = append(parts, fmt.Sprintf("Missing fields: %v", sm.MissingFields))
|
||||
}
|
||||
if len(sm.UnknownFields) > 0 {
|
||||
parts = append(parts, fmt.Sprintf("Unknown fields: %v", sm.UnknownFields))
|
||||
}
|
||||
if len(sm.UnknownValues) > 0 {
|
||||
parts = append(parts, fmt.Sprintf("Unknown field structures: %v", sm.UnknownValues))
|
||||
}
|
||||
parts = append(parts, fmt.Sprintf("Endpoint: %s", sm.Endpoint))
|
||||
parts = append(parts, fmt.Sprintf("Library version: %s", sm.LibraryVersion))
|
||||
return strings.Join(parts, "\n")
|
||||
}
|
||||
|
||||
// SchemaMismatchHandler is a callback for handling schema mismatches
|
||||
type SchemaMismatchHandler func(*SchemaMismatch)
|
||||
|
||||
// SchemaMismatchConfig holds configuration for schema mismatch handling
|
||||
type SchemaMismatchConfig struct {
|
||||
ReportUnknownFields bool
|
||||
Strict bool
|
||||
OnSchemaMismatch SchemaMismatchHandler
|
||||
}
|
||||
|
||||
// DefaultSchemaMismatchConfig returns the default configuration
|
||||
func DefaultSchemaMismatchConfig() SchemaMismatchConfig {
|
||||
return SchemaMismatchConfig{
|
||||
ReportUnknownFields: false,
|
||||
Strict: false,
|
||||
OnSchemaMismatch: nil,
|
||||
}
|
||||
}
|
||||
|
||||
// ReportSchemaMismatch reports a schema mismatch according to config
|
||||
func ReportSchemaMismatch(config SchemaMismatchConfig, modelName, endpoint string, missingFields, unknownFields []string, unknownValues map[string]any) error {
|
||||
mismatch := &SchemaMismatch{
|
||||
ModelName: modelName,
|
||||
MissingFields: missingFields,
|
||||
UnknownFields: unknownFields,
|
||||
UnknownValues: unknownValues,
|
||||
Endpoint: endpoint,
|
||||
LibraryVersion: "go-port-0.1.0",
|
||||
}
|
||||
|
||||
if config.OnSchemaMismatch != nil {
|
||||
config.OnSchemaMismatch(mismatch)
|
||||
return nil
|
||||
}
|
||||
|
||||
msg := mismatch.Describe()
|
||||
if config.Strict {
|
||||
return fmt.Errorf("schema mismatch: %s", msg)
|
||||
}
|
||||
|
||||
log.Printf("WARNING: Schema mismatch for %s:\n%s", modelName, msg)
|
||||
return nil
|
||||
}
|
||||
|
||||
// SchemaMismatchTracker tracks reported mismatches to avoid duplicates
|
||||
type SchemaMismatchTracker struct {
|
||||
mu sync.Mutex
|
||||
reported map[string]bool
|
||||
}
|
||||
|
||||
func NewSchemaMismatchTracker() *SchemaMismatchTracker {
|
||||
return &SchemaMismatchTracker{
|
||||
reported: make(map[string]bool),
|
||||
}
|
||||
}
|
||||
|
||||
func (t *SchemaMismatchTracker) ShouldReport(modelName string, fields []string) bool {
|
||||
t.mu.Lock()
|
||||
defer t.mu.Unlock()
|
||||
|
||||
key := modelName + ":" + strings.Join(fields, ",")
|
||||
if t.reported[key] {
|
||||
return false
|
||||
}
|
||||
t.reported[key] = true
|
||||
return true
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
package utils
|
||||
|
||||
import (
|
||||
"crypto/hmac"
|
||||
"crypto/sha256"
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"time"
|
||||
)
|
||||
|
||||
const (
|
||||
// DefaultSignKey is the signing key from the Android app
|
||||
DefaultSignKey = "p93jhgh689SBReK6ghtw62"
|
||||
// DesktopSignKey is the signing key from the desktop app
|
||||
DesktopSignKey = "kzqU4XhfCaY6B6JTHODeq5"
|
||||
// DesktopClient is the X-Yandex-Music-Client header value for desktop
|
||||
DesktopClient = "YandexMusicDesktopAppWindows/5.95.0"
|
||||
)
|
||||
|
||||
// Sign represents a request signature
|
||||
type Sign struct {
|
||||
Timestamp int64
|
||||
Value string
|
||||
}
|
||||
|
||||
// GetSignRequest creates a signature for a track request
|
||||
func GetSignRequest(trackID string, key string) Sign {
|
||||
if key == "" {
|
||||
key = DefaultSignKey
|
||||
}
|
||||
|
||||
timestamp := time.Now().Unix()
|
||||
message := trackID + strconv.FormatInt(timestamp, 10)
|
||||
|
||||
h := hmac.New(sha256.New, []byte(key))
|
||||
h.Write([]byte(message))
|
||||
sign := base64.StdEncoding.EncodeToString(h.Sum(nil))
|
||||
|
||||
return Sign{
|
||||
Timestamp: timestamp,
|
||||
Value: sign,
|
||||
}
|
||||
}
|
||||
|
||||
// GetFileInfoSign creates a signature for file info requests
|
||||
func GetFileInfoSign(trackID, quality string, codecs []string, transport, key string) Sign {
|
||||
if key == "" {
|
||||
key = DesktopSignKey
|
||||
}
|
||||
|
||||
timestamp := time.Now().Unix()
|
||||
message := fmt.Sprintf("%d%s%s%s%s", timestamp, trackID, quality, joinCodecs(codecs), transport)
|
||||
|
||||
h := hmac.New(sha256.New, []byte(key))
|
||||
h.Write([]byte(message))
|
||||
sign := base64.StdEncoding.EncodeToString(h.Sum(nil))
|
||||
// Remove last character as in Python
|
||||
if len(sign) > 0 {
|
||||
sign = sign[:len(sign)-1]
|
||||
}
|
||||
|
||||
return Sign{
|
||||
Timestamp: timestamp,
|
||||
Value: sign,
|
||||
}
|
||||
}
|
||||
|
||||
func joinCodecs(codecs []string) string {
|
||||
result := ""
|
||||
for _, c := range codecs {
|
||||
result += c
|
||||
}
|
||||
return result
|
||||
}
|
||||
@@ -0,0 +1,233 @@
|
||||
package utils
|
||||
|
||||
import (
|
||||
"crypto/hmac"
|
||||
"crypto/sha256"
|
||||
"encoding/base64"
|
||||
"strconv"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
// TestGetSignRequest tests the GetSignRequest function with various inputs
|
||||
func TestGetSignRequest(t *testing.T) {
|
||||
// Test with default key
|
||||
sign := GetSignRequest("12345", "")
|
||||
if sign.Timestamp == 0 {
|
||||
t.Error("Expected non-zero timestamp")
|
||||
}
|
||||
if sign.Value == "" {
|
||||
t.Error("Expected non-empty sign value")
|
||||
}
|
||||
|
||||
// Test with custom key
|
||||
sign = GetSignRequest("12345", "custom-key")
|
||||
if sign.Timestamp == 0 {
|
||||
t.Error("Expected non-zero timestamp")
|
||||
}
|
||||
if sign.Value == "" {
|
||||
t.Error("Expected non-empty sign value")
|
||||
}
|
||||
|
||||
// Test track ID as string (function accepts string)
|
||||
sign = GetSignRequest("12345", "")
|
||||
if sign.Timestamp == 0 {
|
||||
t.Error("Expected non-zero timestamp for string track ID")
|
||||
}
|
||||
if sign.Value == "" {
|
||||
t.Error("Expected non-empty sign value for string track ID")
|
||||
}
|
||||
}
|
||||
|
||||
// TestGetFileInfoSign tests the GetFileInfoSign function with various inputs
|
||||
func TestGetFileInfoSign(t *testing.T) {
|
||||
codecs := []string{"mp3", "flac"}
|
||||
sign := GetFileInfoSign("12345", "high", codecs, "http", "")
|
||||
if sign.Timestamp == 0 {
|
||||
t.Error("Expected non-zero timestamp")
|
||||
}
|
||||
if sign.Value == "" {
|
||||
t.Error("Expected non-empty sign value")
|
||||
}
|
||||
|
||||
// Test with custom key
|
||||
sign = GetFileInfoSign("12345", "high", codecs, "http", "custom-key")
|
||||
if sign.Timestamp == 0 {
|
||||
t.Error("Expected non-zero timestamp")
|
||||
}
|
||||
if sign.Value == "" {
|
||||
t.Error("Expected non-empty sign value")
|
||||
}
|
||||
|
||||
// Test empty codecs
|
||||
sign = GetFileInfoSign("12345", "high", []string{}, "http", "")
|
||||
if sign.Timestamp == 0 {
|
||||
t.Error("Expected non-zero timestamp for empty codecs")
|
||||
}
|
||||
if sign.Value == "" {
|
||||
t.Error("Expected non-empty sign value for empty codecs")
|
||||
}
|
||||
}
|
||||
|
||||
// TestSignDeterministic tests that same input produces same output with fixed time
|
||||
func TestSignDeterministic(t *testing.T) {
|
||||
// We can't easily test this without mocking time, but we can verify
|
||||
// that the function doesn't panic
|
||||
for i := 0; i < 10; i++ {
|
||||
sign := GetSignRequest("test-track", "test-key")
|
||||
if sign.Value == "" {
|
||||
t.Errorf("Iteration %d: got empty sign", i)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TestGetSignRequestFixedTimestamp tests GetSignRequest with a fixed timestamp
|
||||
// to verify deterministic output matches Python reference implementation
|
||||
func TestGetSignRequestFixedTimestamp(t *testing.T) {
|
||||
// Use a fixed timestamp for deterministic testing
|
||||
fixedTime := time.Unix(1668687184, 0)
|
||||
trackID := "4784420"
|
||||
key := "SUPER_SECRET_KEY"
|
||||
expectedSign := "vssEEweZhgv2Aud0rdH9maOXUC03ZkZ/hlo6bSRN8Qg="
|
||||
|
||||
// Manually compute expected sign with fixed time
|
||||
message := trackID + strconv.FormatInt(fixedTime.Unix(), 10)
|
||||
h := hmac.New(sha256.New, []byte(key))
|
||||
h.Write([]byte(message))
|
||||
computedSign := base64.StdEncoding.EncodeToString(h.Sum(nil))
|
||||
|
||||
if computedSign != expectedSign {
|
||||
t.Errorf("Expected sign %q, got %q", expectedSign, computedSign)
|
||||
}
|
||||
|
||||
// Test that our function produces correct sign when we override time
|
||||
// Note: We can't easily mock time.Now() in Go without refactoring,
|
||||
// so we test the algorithm directly here
|
||||
sign := GetSignRequest(trackID, key)
|
||||
_ = sign // Just verify no panic
|
||||
}
|
||||
|
||||
// TestGetFileInfoSignFixedTimestamp tests GetFileInfoSign with a fixed timestamp
|
||||
func TestGetFileInfoSignFixedTimestamp(t *testing.T) {
|
||||
fixedTime := time.Unix(1668687184, 0)
|
||||
trackID := "4784420:100500"
|
||||
quality := "lossless"
|
||||
codecs := []string{"flac", "aac", "mp3"}
|
||||
transport := "encraw"
|
||||
key := "SUPER_SECRET_KEY"
|
||||
|
||||
// Manually compute expected sign with fixed time (matching Python)
|
||||
message := strconv.FormatInt(fixedTime.Unix(), 10) + trackID + quality + "flacaacmp3" + transport
|
||||
h := hmac.New(sha256.New, []byte(key))
|
||||
h.Write([]byte(message))
|
||||
sign := base64.StdEncoding.EncodeToString(h.Sum(nil))
|
||||
// Remove last character as in Python
|
||||
if len(sign) > 0 {
|
||||
sign = sign[:len(sign)-1]
|
||||
}
|
||||
|
||||
// Test our function with fixed time logic
|
||||
sign2 := GetFileInfoSign(trackID, quality, codecs, transport, key)
|
||||
_ = sign2 // Just verify no panic
|
||||
|
||||
// Verify the algorithm produces expected format
|
||||
if sign == "" {
|
||||
t.Error("Expected non-empty computed sign")
|
||||
}
|
||||
if sign[len(sign)-1] == '=' {
|
||||
t.Error("Sign should not end with '=' (last char removed)")
|
||||
}
|
||||
}
|
||||
|
||||
// TestJoinCodecs tests the joinCodecs helper function
|
||||
func TestJoinCodecs(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
codecs []string
|
||||
expected string
|
||||
}{
|
||||
{"empty", []string{}, ""},
|
||||
{"single", []string{"mp3"}, "mp3"},
|
||||
{"multiple", []string{"flac", "aac", "mp3"}, "flacaacmp3"},
|
||||
{"with_special_chars", []string{"opus", "aac+"}, "opusaac+"},
|
||||
}
|
||||
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
result := joinCodecs(tc.codecs)
|
||||
if result != tc.expected {
|
||||
t.Errorf("joinCodecs(%v) = %q, want %q", tc.codecs, result, tc.expected)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestSignWithDifferentKeys tests that different keys produce different signatures
|
||||
func TestSignWithDifferentKeys(t *testing.T) {
|
||||
trackID := "12345"
|
||||
key1 := "key1"
|
||||
key2 := "key2"
|
||||
|
||||
sign1 := GetSignRequest(trackID, key1)
|
||||
sign2 := GetSignRequest(trackID, key2)
|
||||
|
||||
if sign1.Value == sign2.Value {
|
||||
t.Error("Different keys should produce different signatures")
|
||||
}
|
||||
}
|
||||
|
||||
// TestFileInfoSignWithDifferentKeys tests that different keys produce different signatures
|
||||
func TestFileInfoSignWithDifferentKeys(t *testing.T) {
|
||||
codecs := []string{"mp3", "flac"}
|
||||
key1 := "key1"
|
||||
key2 := "key2"
|
||||
|
||||
sign1 := GetFileInfoSign("12345", "high", codecs, "http", key1)
|
||||
sign2 := GetFileInfoSign("12345", "high", codecs, "http", key2)
|
||||
|
||||
if sign1.Value == sign2.Value {
|
||||
t.Error("Different keys should produce different signatures")
|
||||
}
|
||||
}
|
||||
|
||||
// TestFileInfoSignRemovesLastChar tests that the last character is removed from base64 output
|
||||
func TestFileInfoSignRemovesLastChar(t *testing.T) {
|
||||
codecs := []string{"mp3"}
|
||||
sign := GetFileInfoSign("12345", "high", codecs, "http", "test-key")
|
||||
|
||||
// The sign should not end with '=' (padding) because we remove the last char
|
||||
// Note: it could end with '=' if the original base64 ended with '==', but we remove one char
|
||||
_ = sign
|
||||
// This is more of a sanity check that the function runs
|
||||
}
|
||||
|
||||
// TestDefaultSignKey tests that default keys are used when empty string provided
|
||||
func TestDefaultSignKey(t *testing.T) {
|
||||
// GetSignRequest should use DefaultSignKey when key is empty
|
||||
sign1 := GetSignRequest("12345", "")
|
||||
sign2 := GetSignRequest("12345", DefaultSignKey)
|
||||
|
||||
// They should produce same result (timestamps will differ, but algorithm same)
|
||||
if sign1.Value == "" || sign2.Value == "" {
|
||||
t.Error("Both should produce non-empty signatures")
|
||||
}
|
||||
|
||||
// GetFileInfoSign should use DesktopSignKey when key is empty
|
||||
sign3 := GetFileInfoSign("12345", "high", []string{"mp3"}, "http", "")
|
||||
sign4 := GetFileInfoSign("12345", "high", []string{"mp3"}, "http", DesktopSignKey)
|
||||
|
||||
if sign3.Value == "" || sign4.Value == "" {
|
||||
t.Error("Both should produce non-empty signatures")
|
||||
}
|
||||
}
|
||||
|
||||
// TestTimestampIsRecent tests that timestamp is close to current time
|
||||
func TestTimestampIsRecent(t *testing.T) {
|
||||
before := time.Now().Unix()
|
||||
sign := GetSignRequest("12345", "test-key")
|
||||
after := time.Now().Unix()
|
||||
|
||||
if sign.Timestamp < before || sign.Timestamp > after {
|
||||
t.Errorf("Timestamp %d not in range [%d, %d]", sign.Timestamp, before, after)
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user