server,media-export: Allow container creation
[profile/ivi/rygel.git] / tests / rygel-object-creator-test.vala
1 /*
2  * Copyright (C) 2012 Nokia Corporation.
3  * Copyright (C) 2012 Intel Corporation.
4  *
5  * Author: Jens Georg <jensg@openismus.com>
6  *
7  * This file is part of Rygel.
8  *
9  * Rygel is free software; you can redistribute it and/or modify
10  * it under the terms of the GNU Lesser General Public License as published by
11  * the Free Software Foundation; either version 2 of the License, or
12  * (at your option) any later version.
13  *
14  * Rygel is distributed in the hope that it will be useful,
15  * but WITHOUT ANY WARRANTY; without even the implied warranty of
16  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
17  * GNU Lesser General Public License for more details.
18  *
19  * You should have received a copy of the GNU Lesser General Public License
20  * along with this program; if not, write to the Free Software Foundation,
21  * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
22  */
23
24 /* This is not used.
25 [CCode (cname = "uuid_generate", cheader_filename = "uuid/uuid.h")]
26 internal extern static void uuid_generate ([CCode (array_length = false)]
27                                            uchar[] uuid);
28 [CCode (cname = "uuid_unparse", cheader_filename = "uuid/uuid.h")]
29 internal extern static void uuid_unparse ([CCode (array_length = false)]
30                                           uchar[] uuid,
31                                           [CCode (array_length = false)]
32                                           uchar[] output);
33 */
34
35 public const string DIDL_ITEM = """<?xml version="1.0" encoding="UTF-8"?>
36 <DIDL-Lite
37     xmlns:dc="http://purl.org/dc/elements/1.1/"
38     xmlns="urn:schemas-upnp-org:metadata-1-0/DIDL-Lite/"
39     xmlns:upnp="urn:schemas-upnp-org:metadata-1-0/upnp/"
40     xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
41     xsi:schemaLocation="urn:schemas-upnp-org:metadata-1-0/DIDL-Lite/
42         http://www.upnp.org/schemas/av/didl-lite-v2-20060531.xsd
43       urn:schemas-upnp-org:metadata-1-0/upnp/
44         http://www.upnp.org/schemas/av/upnp-v2-20060531.xsd">
45     <item id="" parentID="0" restricted="0">
46         <dc:title>New Song</dc:title>
47         <upnp:class>object.item.audioItem</upnp:class>
48         <res protocolInfo="*:*:*:*" />
49     </item>
50 </DIDL-Lite>""";
51
52 public class Rygel.ServiceAction : GLib.Object {
53     public int error_code;
54     public string error_message;
55     public string id;
56     public string elements;
57
58     public ServiceAction (string? container_id,
59                           string? elements) {
60         this.id = container_id;
61         this.elements = elements;
62     }
63
64     public void @return() {}
65     public void return_error (int code, string message) {
66         this.error_code = code;
67         this.error_message = message;
68     }
69
70     public new void @get (string arg1_name,
71                           Type arg1_type,
72                           out string arg1_val,
73                           string arg2_name,
74                           Type arg2_type,
75                           out string arg2_val) {
76         assert (arg1_name == "ContainerID");
77         assert (arg1_type == typeof (string));
78         arg1_val = id;
79
80         assert (arg2_name == "Elements");
81         assert (arg2_type == typeof (string));
82         arg2_val = elements;
83     }
84
85     public new void @set (string arg1_name,
86                           Type arg1_type,
87                           string arg1_val,
88                           string arg2_name,
89                           Type arg2_type,
90                           string arg2_val) {
91         assert (arg1_name == "ObjectID");
92         assert (arg1_type == typeof (string));
93
94         assert (arg2_name == "Result");
95         assert (arg2_type == typeof (string));
96     }
97 }
98
99 public class Rygel.HTTPServer : GLib.Object {
100 }
101
102 public class Rygel.ObjectRemovalQueue : GLib.Object {
103     public static ObjectRemovalQueue get_default () {
104         return new ObjectRemovalQueue ();
105     }
106
107     public void queue (MediaObject object, Cancellable? cancellable) {
108     }
109 }
110
111 public class Rygel.MediaObject : GLib.Object {
112     public string id {get; set; }
113     public string ref_id;
114     public unowned MediaContainer parent { get; set; }
115     public string upnp_class;
116     public string title { get; set; }
117     public GUPnP.OCMFlags ocm_flags;
118     public Gee.ArrayList<string> uris;
119     public uint object_update_id;
120
121     public void add_uri (string uri) {
122         this.uris.add (uri);
123     }
124
125     internal void serialize (Rygel.Serializer serializer, HTTPServer server) {
126     }
127
128     public virtual async MediaObjects? get_children
129                                             (uint         offset,
130                                              uint         max_count,
131                                              string       sort_criteria,
132                                              Cancellable? cancellable)
133                                             throws Error {
134         return null;
135     }
136
137     public virtual async MediaObject? find_object (string       id,
138                                                    Cancellable? cancellable)
139                                                    throws Error {
140         return null;
141     }
142 }
143
144 public interface Rygel.TrackableContainer : Rygel.MediaContainer {
145 }
146
147 public interface Rygel.TrackableItem : Rygel.MediaItem {
148 }
149
150 public class Rygel.MediaItem : Rygel.MediaObject {
151     public string dlna_profile;
152     public string mime_type;
153     public long size;
154     public bool place_holder;
155     public string date;
156
157     public MediaItem (string id, MediaContainer parent, string title) {
158         this.id = id;
159         this.parent = parent;
160         this.title = title;
161     }
162
163 }
164
165 public class Rygel.MusicItem : Rygel.AudioItem {
166     public new const string UPNP_CLASS = "object.item.audioItem.musicTrack";
167
168     public MusicItem (string id, MediaContainer parent, string title) {
169         base (id, parent, title);
170     }
171 }
172
173 public class Rygel.AudioItem : Rygel.MediaItem {
174     public const string UPNP_CLASS = "object.item.audioItem";
175     public string artist;
176     public string album;
177
178     public AudioItem (string id, MediaContainer parent, string title) {
179         base (id, parent, title);
180     }
181 }
182 public class Rygel.ImageItem : Rygel.MediaItem {
183     public new const string UPNP_CLASS = "object.item.imageItem";
184     public ImageItem (string id, MediaContainer parent, string title) {
185         base (id, parent, title);
186     }
187 }
188
189 public class Rygel.VideoItem : Rygel.MediaItem {
190     public const string UPNP_CLASS = "object.item.videoItem";
191     public VideoItem (string id, MediaContainer parent, string title) {
192         base (id, parent, title);
193     }
194 }
195
196 public class Rygel.PhotoItem : Rygel.MediaItem {
197     public const string UPNP_CLASS = "object.item.imageItem.photo";
198     public string creator;
199
200     public PhotoItem (string id, MediaContainer parent, string title) {
201         base (id, parent, title);
202     }
203 }
204
205 public class Rygel.PlaylistItem : Rygel.MediaItem {
206     public const string UPNP_CLASS = "object.item.playlistItem";
207
208     public PlaylistItem (string id, MediaContainer parent, string title) {
209         base (id, parent, title);
210     }
211 }
212
213 public class Rygel.ContentDirectory : GLib.Object {
214     public Cancellable cancellable;
215     public MediaContainer root_container;
216     public HTTPServer http_server;
217 }
218
219 public class Rygel.MediaContainer : Rygel.MediaObject {
220     public Gee.ArrayList<string> create_classes = new Gee.ArrayList<string> ();
221     public int child_count { get; set; }
222     public string sort_criteria = "+dc:title";
223     public static const string ANY = "DLNA.ORG_AnyContainer";
224     public static const string STORAGE_FOLDER =
225         "object.container.storageFolder";
226     public static const string PLAYLIST =
227         "object.container.playlistContainer";
228     public uint update_id;
229
230     // mockable elements
231     public MediaObject found_object = null;
232
233     public override async MediaObject? find_object (string       id,
234                                            Cancellable? cancellable = null)
235                                            throws Error {
236         Idle.add (() => { find_object.callback (); return false; });
237         yield;
238
239         return found_object;
240     }
241
242     public signal void container_updated (MediaContainer container);
243 }
244
245 public class Rygel.MediaObjects : Gee.ArrayList<MediaObject> {
246 }
247
248 public class Rygel.WritableContainer : Rygel.MediaContainer {
249     public bool can_create (string upnp_class) {
250         return this.create_classes.contains (upnp_class);
251     }
252
253     public async File? get_writable (Cancellable? cancellable = null) {
254         return File.new_for_commandline_arg ("/tmp");
255     }
256
257     public async void add_item (MediaItem    item,
258                                 Cancellable? cancellable = null) {
259     }
260
261     public async void add_container (MediaContainer container, Cancellable?
262             cancellable = null) { }
263 }
264
265 public class Rygel.SearchableContainer : Rygel.MediaContainer {
266     public MediaObjects result = new MediaObjects ();
267
268     public async MediaObjects search (SearchExpression expression,
269                                       int              offset,
270                                       int              count,
271                                       out int          total_matches,
272                                       string           soer_criteria,
273                                       Cancellable?     cancellable = null) {
274         total_matches = 0;
275         Idle.add (() => { search.callback (); return false; });
276         yield;
277
278         return result;
279     }
280 }
281
282 public errordomain Rygel.ContentDirectoryError {
283     BAD_METADATA,
284     NO_SUCH_OBJECT,
285     INVALID_ARGS,
286     RESTRICTED_PARENT,
287     ERROR
288 }
289
290 public class Rygel.Transcoder {
291 }
292
293 public class Rygel.TestMediaEngine : Rygel.MediaEngine {
294     private GLib.List<DLNAProfile> dlna_profiles = new GLib.List<DLNAProfile>();
295
296     public override unowned GLib.List<DLNAProfile> get_dlna_profiles () {
297         return dlna_profiles;
298     }
299
300     public override unowned GLib.List<Transcoder>? get_transcoders () {
301         return null;
302     }
303
304     public override DataSource? create_data_source (string uri) {
305         return null;
306     }
307 }
308
309 public class Rygel.EngineLoader {
310     public EngineLoader () { }
311
312     public MediaEngine load_engine () {
313         return new TestMediaEngine ();
314     }
315 }
316
317 public static void log_func (string? domain,
318                              LogLevelFlags flags,
319                              string message) {
320
321     // Ignore critical of gee 0.6 and recent glib
322     if (message.has_prefix ("Read-only property 'read-only-view' on class")) {
323         Log.default_handler (domain, flags, message);
324
325         return;
326     }
327
328     if (LogLevelFlags.LEVEL_CRITICAL in flags ||
329         LogLevelFlags.LEVEL_ERROR in flags ||
330         LogLevelFlags.FLAG_FATAL in flags) {
331         print ("======> FAILED: %s: %s\n", domain ?? "", message);
332         assert_not_reached ();
333     }
334 }
335
336 public class Rygel.HTTPObjectCreatorTest : GLib.Object {
337
338     public static int main (string[] args) {
339         Log.set_default_handler (log_func);
340         var test = new HTTPObjectCreatorTest ();
341         test.test_parse_args ();
342         test.test_didl_parsing ();
343         test.test_fetch_container ();
344
345         /* This is just here to avoid warnings about unused methods: */
346         var serializer = new Serializer (SerializerType.GENERIC_DIDL);
347         serializer.add_item ();
348         serializer.add_container ();
349         serializer.filter ("something");
350
351         return 0;
352     }
353
354     // expected errors
355     Error no_such_object;
356     Error restricted_parent;
357     Error bad_metadata;
358     Error invalid_args;
359
360     public HTTPObjectCreatorTest () {
361         this.no_such_object = new ContentDirectoryError.NO_SUCH_OBJECT("");
362         this.restricted_parent = new ContentDirectoryError.RESTRICTED_PARENT("");
363         this.bad_metadata = new ContentDirectoryError.BAD_METADATA("");
364         this.invalid_args = new ContentDirectoryError.INVALID_ARGS("");
365     }
366
367     private void test_parse_args () {
368         // check null container id
369         var content_directory = new ContentDirectory ();
370
371         var action = new ServiceAction (null, "");
372         var creator = new ObjectCreator (content_directory, action);
373         creator.run.begin ();
374         assert (action.error_code == no_such_object.code);
375
376         // check elements containing a comment
377         action = new ServiceAction ("0", "<!-- This is an XML comment -->");
378         creator = new ObjectCreator (content_directory, action);
379         creator.run.begin ();
380         assert (action.error_code == bad_metadata.code);
381
382         // check null elements
383         action = new ServiceAction ("0", null);
384         creator = new ObjectCreator (content_directory, action);
385         creator.run.begin ();
386         assert (action.error_code == bad_metadata.code);
387     }
388
389     private void test_didl_parsing_step (Xml.Doc *doc, int expected_code) {
390         string xml;
391
392         doc->dump_memory_enc (out xml);
393         var action = new ServiceAction ("0", xml);
394         var content_directory = new ContentDirectory ();
395         var creator = new ObjectCreator (content_directory, action);
396         creator.run.begin ();
397         assert (action.error_code == expected_code);
398     }
399
400     private void test_didl_parsing () {
401         var xml = Xml.Parser.read_memory (DIDL_ITEM,
402                                           DIDL_ITEM.length,
403                                           null,
404                                           null,
405                                           Xml.ParserOption.RECOVER |
406                                           Xml.ParserOption.NOBLANKS);
407         var didl_node = xml->children;
408         var item_node = didl_node->children;
409         var content_directory = new ContentDirectory ();
410
411         // test no DIDL
412         var action = new ServiceAction ("0", "");
413         var creator = new ObjectCreator (content_directory, action);
414         creator.run.begin ();
415         assert (action.error_code == bad_metadata.code);
416         assert (action.error_message == "Bad metadata");
417
418         // test empty DIDL
419         item_node->unlink ();
420         didl_node->set_content ("  ");
421         this.test_didl_parsing_step (xml, bad_metadata.code);
422
423         // test item node with missing restricted attribute
424         var tmp = item_node->copy (1);
425         tmp->unset_prop ("restricted");
426         didl_node->add_child (tmp);
427         this.test_didl_parsing_step (xml, bad_metadata.code);
428
429         // test item node with restricted=1
430         tmp->set_prop ("restricted", "1");
431         this.test_didl_parsing_step (xml, invalid_args.code);
432
433         // test item node with invalid id
434         tmp->unlink ();
435         tmp = item_node->copy (1);
436         tmp->set_prop ("id", "InvalidItemId");
437         didl_node->add_child (tmp);
438         this.test_didl_parsing_step (xml, bad_metadata.code);
439
440         // test item node with missing id
441         tmp->unset_prop ("id");
442         this.test_didl_parsing_step (xml, bad_metadata.code);
443
444         // test item node with missing title
445         tmp->unlink ();
446         tmp = item_node->copy (1);
447         var title_node = tmp->children;
448         title_node->unlink ();
449         didl_node->add_child (tmp);
450         this.test_didl_parsing_step (xml, bad_metadata.code);
451
452         // test missing or empty upnp class
453         tmp->unlink ();
454         tmp = item_node->copy (1);
455         var class_node = tmp->children->next;
456
457         class_node->set_content ("");
458         this.test_didl_parsing_step (xml, bad_metadata.code);
459
460         class_node->unlink ();
461         this.test_didl_parsing_step (xml, bad_metadata.code);
462     }
463
464     private void test_fetch_container_run (ObjectCreator creator) {
465         var main_loop = new MainLoop (null, false);
466         creator.run.begin ( () => { main_loop.quit (); });
467         main_loop.run ();
468     }
469
470     private void test_fetch_container () {
471         // check case when object is not found
472         var content_directory = new ContentDirectory ();
473         var root_container = new SearchableContainer ();
474         content_directory.root_container = root_container;
475         var action = new ServiceAction ("0", DIDL_ITEM);
476         var creator = new ObjectCreator (content_directory, action);
477         this.test_fetch_container_run (creator);
478         assert (action.error_code == no_such_object.code);
479
480         // check case when found object is not a container → Error 710
481         // cf. ContentDirectory:2 spec, Table 2-22
482         root_container.found_object = new MediaObject ();
483         this.test_fetch_container_run (creator);
484         assert (action.error_code == no_such_object.code);
485
486         // check case when found container does not have OCMUpload set
487         root_container.found_object = new MediaContainer ();
488         this.test_fetch_container_run (creator);
489         assert (action.error_code == restricted_parent.code);
490
491         // check case when found container is not a writable container
492         root_container.found_object.ocm_flags |= GUPnP.OCMFlags.UPLOAD;
493         this.test_fetch_container_run (creator);
494         assert (action.error_code == restricted_parent.code);
495
496         // check when found container does not have the correct create class
497         var container = new WritableContainer ();
498         container.create_classes.add ("object.item.imageItem.musicTrack");
499         container.ocm_flags |= GUPnP.OCMFlags.UPLOAD;
500         root_container.found_object = container;
501         this.test_fetch_container_run (creator);
502         assert (action.error_code == bad_metadata.code);
503
504         // check DLNA.ORG_AnyContainer when root container is not searchable
505         content_directory.root_container = new MediaContainer ();
506         action.id = "DLNA.ORG_AnyContainer";
507         this.test_fetch_container_run (creator);
508         assert (action.error_code == no_such_object.code);
509
510         // check DLNA.ORG_AnyContainer when no writable container is found
511         content_directory.root_container = new SearchableContainer ();
512         this.test_fetch_container_run (creator);
513         // We cannot distinguish this case from the "create-class doesn't match"
514         // case
515         assert (action.error_code == bad_metadata.code);
516     }
517 }