diff --git a/core/transcode_decision.go b/core/transcode_decision.go index 73f52924c..3ab74fff3 100644 --- a/core/transcode_decision.go +++ b/core/transcode_decision.go @@ -64,6 +64,34 @@ type Limitation struct { Required bool } +// Protocol values (OpenSubsonic spec enum) +const ( + ProtocolHTTP = "http" + ProtocolHLS = "hls" +) + +// Comparison operators (OpenSubsonic spec enum) +const ( + ComparisonEquals = "Equals" + ComparisonNotEquals = "NotEquals" + ComparisonLessThanEqual = "LessThanEqual" + ComparisonGreaterThanEqual = "GreaterThanEqual" +) + +// Limitation names (OpenSubsonic spec enum) +const ( + LimitationAudioChannels = "audioChannels" + LimitationAudioBitrate = "audioBitrate" + LimitationAudioProfile = "audioProfile" + LimitationAudioSamplerate = "audioSamplerate" + LimitationAudioBitdepth = "audioBitdepth" +) + +// Codec profile types (OpenSubsonic spec enum) +const ( + CodecProfileTypeAudio = "AudioCodec" +) + // Decision represents the internal decision result. // All bitrate values are in kilobits per second (kbps). type Decision struct { @@ -84,6 +112,7 @@ type Decision struct { type StreamDetails struct { Container string Codec string + Profile string // Audio profile (e.g., "LC", "HE-AAC"). Empty until scanner support is added. Bitrate int SampleRate int BitDepth int @@ -179,7 +208,7 @@ func (s *transcodeDecisionService) MakeDecision(ctx context.Context, mf *model.M // or a typed reason string if it doesn't match. func (s *transcodeDecisionService) checkDirectPlayProfile(mf *model.MediaFile, sourceBitrate int, profile *DirectPlayProfile, clientInfo *ClientInfo) string { // Check protocol (only http for now) - if len(profile.Protocols) > 0 && !containsIgnoreCase(profile.Protocols, "http") { + if len(profile.Protocols) > 0 && !containsIgnoreCase(profile.Protocols, ProtocolHTTP) { return "protocol not supported" } @@ -200,7 +229,7 @@ func (s *transcodeDecisionService) checkDirectPlayProfile(mf *model.MediaFile, s // Check codec-specific limitations for _, codecProfile := range clientInfo.CodecProfiles { - if strings.EqualFold(codecProfile.Type, "AudioCodec") && matchesCodec(mf.AudioCodec(), []string{codecProfile.Name}) { + if strings.EqualFold(codecProfile.Type, CodecProfileTypeAudio) && matchesCodec(mf.AudioCodec(), []string{codecProfile.Name}) { if reason := checkLimitations(mf, sourceBitrate, codecProfile.Limitations); reason != "" { return reason } @@ -214,31 +243,37 @@ func (s *transcodeDecisionService) checkDirectPlayProfile(mf *model.MediaFile, s // Returns "" if all limitations pass, or a typed reason string for the first failure. func checkLimitations(mf *model.MediaFile, sourceBitrate int, limitations []Limitation) string { for _, lim := range limitations { - switch strings.ToLower(lim.Name) { - case "audiochannels": + if strings.EqualFold(lim.Name, LimitationAudioChannels) { if !checkIntLimitation(mf.Channels, lim.Comparison, lim.Values) { if lim.Required { return "audio channels not supported" } } - case "audiosamplerate": + } else if strings.EqualFold(lim.Name, LimitationAudioSamplerate) { if !checkIntLimitation(mf.SampleRate, lim.Comparison, lim.Values) { if lim.Required { return "audio samplerate not supported" } } - case "audiobitrate": + } else if strings.EqualFold(lim.Name, LimitationAudioBitrate) { if !checkIntLimitation(sourceBitrate, lim.Comparison, lim.Values) { if lim.Required { return "audio bitrate not supported" } } - case "audiobitdepth": + } else if strings.EqualFold(lim.Name, LimitationAudioBitdepth) { if !checkIntLimitation(mf.BitDepth, lim.Comparison, lim.Values) { if lim.Required { return "audio bitdepth not supported" } } + } else if strings.EqualFold(lim.Name, LimitationAudioProfile) { + // TODO: populate source profile when MediaFile has audio profile info + if !checkStringLimitation("", lim.Comparison, lim.Values) { + if lim.Required { + return "audio profile not supported" + } + } } } return "" @@ -257,7 +292,7 @@ const ( // Returns nil if the profile cannot produce a valid output. func (s *transcodeDecisionService) computeTranscodedStream(ctx context.Context, mf *model.MediaFile, sourceBitrate int, profile *TranscodingProfile, clientInfo *ClientInfo) *StreamDetails { // Check protocol (only http for now) - if profile.Protocol != "" && !strings.EqualFold(profile.Protocol, "http") { + if profile.Protocol != "" && !strings.EqualFold(profile.Protocol, ProtocolHTTP) { return nil } @@ -324,7 +359,7 @@ func (s *transcodeDecisionService) computeTranscodedStream(ctx context.Context, // Apply codec profile limitations to the TARGET codec (#4) targetCodec := ts.Codec for _, codecProfile := range clientInfo.CodecProfiles { - if !strings.EqualFold(codecProfile.Type, "AudioCodec") { + if !strings.EqualFold(codecProfile.Type, CodecProfileTypeAudio) { continue } if !matchesCodec(targetCodec, []string{codecProfile.Name}) { @@ -333,7 +368,7 @@ func (s *transcodeDecisionService) computeTranscodedStream(ctx context.Context, for _, lim := range codecProfile.Limitations { result := applyLimitation(sourceBitrate, &lim, ts) // For lossless codecs, adjusting bitrate is not valid - if strings.EqualFold(lim.Name, "audiobitrate") && targetIsLossless && result == adjustAdjusted { + if strings.EqualFold(lim.Name, LimitationAudioBitrate) && targetIsLossless && result == adjustAdjusted { return nil } if result == adjustCannotFit { @@ -348,22 +383,22 @@ func (s *transcodeDecisionService) computeTranscodedStream(ctx context.Context, // applyLimitation adjusts a transcoded stream parameter to satisfy the limitation. // Returns the adjustment result. func applyLimitation(sourceBitrate int, lim *Limitation, ts *StreamDetails) adjustResult { - switch strings.ToLower(lim.Name) { - case "audiochannels": - current := ts.Channels - return applyIntLimitation(lim.Comparison, lim.Values, current, func(v int) { ts.Channels = v }) - case "audiobitrate": + if strings.EqualFold(lim.Name, LimitationAudioChannels) { + return applyIntLimitation(lim.Comparison, lim.Values, ts.Channels, func(v int) { ts.Channels = v }) + } else if strings.EqualFold(lim.Name, LimitationAudioBitrate) { current := ts.Bitrate if current == 0 { current = sourceBitrate } return applyIntLimitation(lim.Comparison, lim.Values, current, func(v int) { ts.Bitrate = v }) - case "audiosamplerate": + } else if strings.EqualFold(lim.Name, LimitationAudioSamplerate) { return applyIntLimitation(lim.Comparison, lim.Values, ts.SampleRate, func(v int) { ts.SampleRate = v }) - case "audiobitdepth": + } else if strings.EqualFold(lim.Name, LimitationAudioBitdepth) { if ts.BitDepth > 0 { return applyIntLimitation(lim.Comparison, lim.Values, ts.BitDepth, func(v int) { ts.BitDepth = v }) } + } else if strings.EqualFold(lim.Name, LimitationAudioProfile) { + // TODO: implement when audio profile data is available } return adjustNone } @@ -375,8 +410,7 @@ func applyIntLimitation(comparison string, values []string, current int, setter return adjustNone } - switch strings.ToLower(comparison) { - case "lessthanequal": + if strings.EqualFold(comparison, ComparisonLessThanEqual) { limit, ok := parseInt(values[0]) if !ok { return adjustNone @@ -386,8 +420,7 @@ func applyIntLimitation(comparison string, values []string, current int, setter } setter(limit) return adjustAdjusted - - case "greaterthanequal": + } else if strings.EqualFold(comparison, ComparisonGreaterThanEqual) { limit, ok := parseInt(values[0]) if !ok { return adjustNone @@ -397,8 +430,7 @@ func applyIntLimitation(comparison string, values []string, current int, setter } // Cannot upscale return adjustCannotFit - - case "equals": + } else if strings.EqualFold(comparison, ComparisonEquals) { // Check if current value matches any allowed value for _, v := range values { if limit, ok := parseInt(v); ok && current == limit { @@ -421,8 +453,7 @@ func applyIntLimitation(comparison string, values []string, current int, setter return adjustAdjusted } return adjustCannotFit - - case "notequals": + } else if strings.EqualFold(comparison, ComparisonNotEquals) { for _, v := range values { if limit, ok := parseInt(v); ok && current == limit { return adjustCannotFit @@ -573,36 +604,56 @@ func checkIntLimitation(value int, comparison string, values []string) bool { return true } - switch strings.ToLower(comparison) { - case "lessthanequal": + if strings.EqualFold(comparison, ComparisonLessThanEqual) { limit, ok := parseInt(values[0]) if !ok { return true } return value <= limit - case "greaterthanequal": + } else if strings.EqualFold(comparison, ComparisonGreaterThanEqual) { limit, ok := parseInt(values[0]) if !ok { return true } return value >= limit - case "equals": + } else if strings.EqualFold(comparison, ComparisonEquals) { for _, v := range values { if limit, ok := parseInt(v); ok && value == limit { return true } } return false - case "notequals": + } else if strings.EqualFold(comparison, ComparisonNotEquals) { for _, v := range values { if limit, ok := parseInt(v); ok && value == limit { return false } } return true - default: + } + return true +} + +// checkStringLimitation checks a string value against a limitation. +// Only Equals and NotEquals comparisons are meaningful for strings. +// LessThanEqual/GreaterThanEqual are not applicable and always pass. +func checkStringLimitation(value string, comparison string, values []string) bool { + if strings.EqualFold(comparison, ComparisonEquals) { + for _, v := range values { + if strings.EqualFold(value, v) { + return true + } + } + return false + } else if strings.EqualFold(comparison, ComparisonNotEquals) { + for _, v := range values { + if strings.EqualFold(value, v) { + return false + } + } return true } + return true } func parseInt(s string) (int, bool) { diff --git a/core/transcode_decision_test.go b/core/transcode_decision_test.go index 8e93148fd..5850292c6 100644 --- a/core/transcode_decision_test.go +++ b/core/transcode_decision_test.go @@ -331,10 +331,10 @@ var _ = Describe("TranscodeDecision", func() { }, CodecProfiles: []CodecProfile{ { - Type: "AudioCodec", + Type: CodecProfileTypeAudio, Name: "mp3", Limitations: []Limitation{ - {Name: "audioBitrate", Comparison: "LessThanEqual", Values: []string{"320"}, Required: true}, + {Name: LimitationAudioBitrate, Comparison: ComparisonLessThanEqual, Values: []string{"320"}, Required: true}, }, }, }, @@ -353,10 +353,10 @@ var _ = Describe("TranscodeDecision", func() { }, CodecProfiles: []CodecProfile{ { - Type: "AudioCodec", + Type: CodecProfileTypeAudio, Name: "mp3", Limitations: []Limitation{ - {Name: "audioBitrate", Comparison: "LessThanEqual", Values: []string{"320"}, Required: false}, + {Name: LimitationAudioBitrate, Comparison: ComparisonLessThanEqual, Values: []string{"320"}, Required: false}, }, }, }, @@ -374,10 +374,10 @@ var _ = Describe("TranscodeDecision", func() { }, CodecProfiles: []CodecProfile{ { - Type: "AudioCodec", + Type: CodecProfileTypeAudio, Name: "flac", Limitations: []Limitation{ - {Name: "audioChannels", Comparison: "Equals", Values: []string{"1", "2"}, Required: true}, + {Name: LimitationAudioChannels, Comparison: ComparisonEquals, Values: []string{"1", "2"}, Required: true}, }, }, }, @@ -395,10 +395,10 @@ var _ = Describe("TranscodeDecision", func() { }, CodecProfiles: []CodecProfile{ { - Type: "AudioCodec", + Type: CodecProfileTypeAudio, Name: "flac", Limitations: []Limitation{ - {Name: "audioChannels", Comparison: "Equals", Values: []string{"1", "2"}, Required: true}, + {Name: LimitationAudioChannels, Comparison: ComparisonEquals, Values: []string{"1", "2"}, Required: true}, }, }, }, @@ -408,6 +408,50 @@ var _ = Describe("TranscodeDecision", func() { Expect(decision.CanDirectPlay).To(BeFalse()) }) + It("rejects direct play when audioProfile limitation fails (required)", func() { + mf := &model.MediaFile{ID: "1", Suffix: "m4a", Codec: "AAC", BitRate: 256, Channels: 2, SampleRate: 44100} + ci := &ClientInfo{ + DirectPlayProfiles: []DirectPlayProfile{ + {Containers: []string{"m4a"}, AudioCodecs: []string{"aac"}, Protocols: []string{"http"}}, + }, + CodecProfiles: []CodecProfile{ + { + Type: CodecProfileTypeAudio, + Name: "aac", + Limitations: []Limitation{ + {Name: LimitationAudioProfile, Comparison: ComparisonEquals, Values: []string{"LC"}, Required: true}, + }, + }, + }, + } + // Source profile is empty (not yet populated from scanner), so Equals("LC") fails + decision, err := svc.MakeDecision(ctx, mf, ci) + Expect(err).ToNot(HaveOccurred()) + Expect(decision.CanDirectPlay).To(BeFalse()) + Expect(decision.TranscodeReasons).To(ContainElement("audio profile not supported")) + }) + + It("allows direct play when audioProfile limitation is optional", func() { + mf := &model.MediaFile{ID: "1", Suffix: "m4a", Codec: "AAC", BitRate: 256, Channels: 2, SampleRate: 44100} + ci := &ClientInfo{ + DirectPlayProfiles: []DirectPlayProfile{ + {Containers: []string{"m4a"}, AudioCodecs: []string{"aac"}, Protocols: []string{"http"}}, + }, + CodecProfiles: []CodecProfile{ + { + Type: CodecProfileTypeAudio, + Name: "aac", + Limitations: []Limitation{ + {Name: LimitationAudioProfile, Comparison: ComparisonEquals, Values: []string{"LC"}, Required: false}, + }, + }, + }, + } + decision, err := svc.MakeDecision(ctx, mf, ci) + Expect(err).ToNot(HaveOccurred()) + Expect(decision.CanDirectPlay).To(BeTrue()) + }) + It("rejects direct play due to samplerate limitation", func() { mf := &model.MediaFile{ID: "1", Suffix: "flac", Codec: "FLAC", BitRate: 1000, Channels: 2, SampleRate: 96000, BitDepth: 24} ci := &ClientInfo{ @@ -416,10 +460,10 @@ var _ = Describe("TranscodeDecision", func() { }, CodecProfiles: []CodecProfile{ { - Type: "AudioCodec", + Type: CodecProfileTypeAudio, Name: "flac", Limitations: []Limitation{ - {Name: "audioSamplerate", Comparison: "LessThanEqual", Values: []string{"48000"}, Required: true}, + {Name: LimitationAudioSamplerate, Comparison: ComparisonLessThanEqual, Values: []string{"48000"}, Required: true}, }, }, }, @@ -441,10 +485,10 @@ var _ = Describe("TranscodeDecision", func() { }, CodecProfiles: []CodecProfile{ { - Type: "AudioCodec", + Type: CodecProfileTypeAudio, Name: "mp3", Limitations: []Limitation{ - {Name: "audioBitrate", Comparison: "LessThanEqual", Values: []string{"96"}, Required: true}, + {Name: LimitationAudioBitrate, Comparison: ComparisonLessThanEqual, Values: []string{"96"}, Required: true}, }, }, }, @@ -464,10 +508,10 @@ var _ = Describe("TranscodeDecision", func() { }, CodecProfiles: []CodecProfile{ { - Type: "AudioCodec", + Type: CodecProfileTypeAudio, Name: "mp3", Limitations: []Limitation{ - {Name: "audioChannels", Comparison: "LessThanEqual", Values: []string{"2"}, Required: true}, + {Name: LimitationAudioChannels, Comparison: ComparisonLessThanEqual, Values: []string{"2"}, Required: true}, }, }, }, @@ -487,10 +531,10 @@ var _ = Describe("TranscodeDecision", func() { }, CodecProfiles: []CodecProfile{ { - Type: "AudioCodec", + Type: CodecProfileTypeAudio, Name: "mp3", Limitations: []Limitation{ - {Name: "audioSamplerate", Comparison: "LessThanEqual", Values: []string{"48000"}, Required: true}, + {Name: LimitationAudioSamplerate, Comparison: ComparisonLessThanEqual, Values: []string{"48000"}, Required: true}, }, }, }, @@ -510,10 +554,10 @@ var _ = Describe("TranscodeDecision", func() { }, CodecProfiles: []CodecProfile{ { - Type: "AudioCodec", + Type: CodecProfileTypeAudio, Name: "mp3", Limitations: []Limitation{ - {Name: "audioSamplerate", Comparison: "GreaterThanEqual", Values: []string{"96000"}, Required: true}, + {Name: LimitationAudioSamplerate, Comparison: ComparisonGreaterThanEqual, Values: []string{"96000"}, Required: true}, }, }, }, diff --git a/server/subsonic/transcode.go b/server/subsonic/transcode.go index 633adbfb6..7f556a0a6 100644 --- a/server/subsonic/transcode.go +++ b/server/subsonic/transcode.go @@ -2,6 +2,7 @@ package subsonic import ( "encoding/json" + "fmt" "net/http" "strconv" "strings" @@ -97,7 +98,7 @@ func (r *clientInfoRequest) toCore() *core.ClientInfo { Required: lim.Required, } // Convert audioBitrate limitation values from bps to kbps - if strings.EqualFold(lim.Name, "audioBitrate") { + if strings.EqualFold(lim.Name, core.LimitationAudioBitrate) { coreLim.Values = convertBitrateValues(lim.Values) } coreCP.Limitations = append(coreCP.Limitations, coreLim) @@ -132,6 +133,59 @@ func convertBitrateValues(bpsValues []string) []string { return result } +// validate checks that all enum fields in the request contain valid values per the OpenSubsonic spec. +func (r *clientInfoRequest) validate() error { + for _, dp := range r.DirectPlayProfiles { + for _, p := range dp.Protocols { + if !isValidProtocol(p) { + return fmt.Errorf("invalid protocol: %s", p) + } + } + } + for _, tp := range r.TranscodingProfiles { + if tp.Protocol != "" && !isValidProtocol(tp.Protocol) { + return fmt.Errorf("invalid protocol: %s", tp.Protocol) + } + } + for _, cp := range r.CodecProfiles { + if !isValidCodecProfileType(cp.Type) { + return fmt.Errorf("invalid codec profile type: %s", cp.Type) + } + for _, lim := range cp.Limitations { + if !isValidLimitationName(lim.Name) { + return fmt.Errorf("invalid limitation name: %s", lim.Name) + } + if !isValidComparison(lim.Comparison) { + return fmt.Errorf("invalid comparison: %s", lim.Comparison) + } + } + } + return nil +} + +func isValidProtocol(p string) bool { + return strings.EqualFold(p, core.ProtocolHTTP) || strings.EqualFold(p, core.ProtocolHLS) +} + +func isValidCodecProfileType(t string) bool { + return strings.EqualFold(t, core.CodecProfileTypeAudio) +} + +func isValidLimitationName(n string) bool { + return strings.EqualFold(n, core.LimitationAudioChannels) || + strings.EqualFold(n, core.LimitationAudioBitrate) || + strings.EqualFold(n, core.LimitationAudioProfile) || + strings.EqualFold(n, core.LimitationAudioSamplerate) || + strings.EqualFold(n, core.LimitationAudioBitdepth) +} + +func isValidComparison(c string) bool { + return strings.EqualFold(c, core.ComparisonEquals) || + strings.EqualFold(c, core.ComparisonNotEquals) || + strings.EqualFold(c, core.ComparisonLessThanEqual) || + strings.EqualFold(c, core.ComparisonGreaterThanEqual) +} + // GetTranscodeDecision handles the OpenSubsonic getTranscodeDecision endpoint. // It receives client capabilities and returns a decision on whether to direct play or transcode. func (api *Router) GetTranscodeDecision(w http.ResponseWriter, r *http.Request) (*responses.Subsonic, error) { @@ -159,13 +213,16 @@ func (api *Router) GetTranscodeDecision(w http.ResponseWriter, r *http.Request) return nil, newError(responses.ErrorGeneric, "mediaType '%s' is not yet supported", mediaType) } - // Parse ClientInfo from request body + // Parse and validate ClientInfo from request body (required per OpenSubsonic spec) var clientInfoReq clientInfoRequest - if r.Body != nil { - if err := json.NewDecoder(r.Body).Decode(&clientInfoReq); err != nil { - log.Debug(ctx, "Failed to parse client info from body", err) - // Continue with empty client info - will likely result in no compatible profile - } + if r.Body == nil { + return nil, newError(responses.ErrorMissingParameter, "missing required JSON request body") + } + if err := json.NewDecoder(r.Body).Decode(&clientInfoReq); err != nil { + return nil, newError(responses.ErrorGeneric, "invalid JSON request body") + } + if err := clientInfoReq.validate(); err != nil { + return nil, newError(responses.ErrorGeneric, "%v", err) } clientInfo := clientInfoReq.toCore() @@ -200,6 +257,7 @@ func (api *Router) GetTranscodeDecision(w http.ResponseWriter, r *http.Request) Container: decision.SourceStream.Container, Codec: decision.SourceStream.Codec, AudioBitrate: int32(kbpsToBps(decision.SourceStream.Bitrate)), + AudioProfile: decision.SourceStream.Profile, AudioSamplerate: int32(decision.SourceStream.SampleRate), AudioBitdepth: int32(decision.SourceStream.BitDepth), AudioChannels: int32(decision.SourceStream.Channels), @@ -212,6 +270,7 @@ func (api *Router) GetTranscodeDecision(w http.ResponseWriter, r *http.Request) Container: decision.TranscodeStream.Container, Codec: decision.TranscodeStream.Codec, AudioBitrate: int32(kbpsToBps(decision.TranscodeStream.Bitrate)), + AudioProfile: decision.TranscodeStream.Profile, AudioSamplerate: int32(decision.TranscodeStream.SampleRate), AudioBitdepth: int32(decision.TranscodeStream.BitDepth), AudioChannels: int32(decision.TranscodeStream.Channels), diff --git a/server/subsonic/transcode_test.go b/server/subsonic/transcode_test.go index 7999709d4..08cfc8b02 100644 --- a/server/subsonic/transcode_test.go +++ b/server/subsonic/transcode_test.go @@ -66,26 +66,48 @@ var _ = Describe("Transcode endpoints", func() { Expect(err).To(HaveOccurred()) }) - It("handles empty body gracefully", func() { - mockMFRepo.SetData(model.MediaFiles{ - {ID: "song-1", Suffix: "mp3", Codec: "MP3", BitRate: 320, Channels: 2, SampleRate: 44100}, - }) - mockTD.decision = &core.Decision{ - MediaID: "song-1", - CanDirectPlay: false, - SourceStream: core.StreamDetails{ - Container: "mp3", Codec: "mp3", Bitrate: 320, - SampleRate: 44100, Channels: 2, - }, - } - mockTD.token = "empty-body-token" - + It("returns error when body is empty", func() { r := newJSONPostRequest("mediaId=song-1&mediaType=song", "") - resp, err := router.GetTranscodeDecision(w, r) + _, err := router.GetTranscodeDecision(w, r) + Expect(err).To(HaveOccurred()) + }) - Expect(err).ToNot(HaveOccurred()) - Expect(resp.TranscodeDecision).ToNot(BeNil()) - Expect(resp.TranscodeDecision.TranscodeParams).To(Equal("empty-body-token")) + It("returns error when body contains invalid JSON", func() { + r := newJSONPostRequest("mediaId=song-1&mediaType=song", "not-json{{{") + _, err := router.GetTranscodeDecision(w, r) + Expect(err).To(HaveOccurred()) + }) + + It("returns error for invalid protocol in direct play profile", func() { + body := `{"directPlayProfiles":[{"containers":["mp3"],"audioCodecs":["mp3"],"protocols":["ftp"]}]}` + r := newJSONPostRequest("mediaId=song-1&mediaType=song", body) + _, err := router.GetTranscodeDecision(w, r) + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(ContainSubstring("invalid protocol")) + }) + + It("returns error for invalid comparison operator", func() { + body := `{"codecProfiles":[{"type":"AudioCodec","name":"mp3","limitations":[{"name":"audioBitrate","comparison":"InvalidOp","values":["320"]}]}]}` + r := newJSONPostRequest("mediaId=song-1&mediaType=song", body) + _, err := router.GetTranscodeDecision(w, r) + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(ContainSubstring("invalid comparison")) + }) + + It("returns error for invalid limitation name", func() { + body := `{"codecProfiles":[{"type":"AudioCodec","name":"mp3","limitations":[{"name":"unknownField","comparison":"Equals","values":["320"]}]}]}` + r := newJSONPostRequest("mediaId=song-1&mediaType=song", body) + _, err := router.GetTranscodeDecision(w, r) + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(ContainSubstring("invalid limitation name")) + }) + + It("returns error for invalid codec profile type", func() { + body := `{"codecProfiles":[{"type":"VideoCodec","name":"mp3"}]}` + r := newJSONPostRequest("mediaId=song-1&mediaType=song", body) + _, err := router.GetTranscodeDecision(w, r) + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(ContainSubstring("invalid codec profile type")) }) It("returns a valid decision response", func() {