mourningdove/t/atom-post.t
2026-05-24 01:03:05 +00:00

653 lines
24 KiB
Perl

# t/atom-post.t
#
# Test post via ATOM protocol.
#
# This code was forked from the LiveJournal project owned and operated
# by Live Journal, Inc. The code has been modified and expanded by
# Dreamwidth Studios, LLC. These files were originally licensed under
# the terms of the license supplied by Live Journal, Inc, which can
# currently be found at:
#
# http://code.livejournal.org/trac/livejournal/browser/trunk/LICENSE-LiveJournal.txt
#
# In accordance with the original license, this code and all its
# modifications are provided under the GNU General Public License.
# A copy of that license can be found in the LICENSE file included as
# part of this distribution.
use strict;
use warnings;
use Test::More;
BEGIN { $LJ::_T_CONFIG = 1; require "$ENV{LJHOME}/cgi-bin/ljlib.pl"; }
use LJ::Test;
plan skip_all => "AtomAPI authentication is currently broken -- remove?";
if (LJ::Test::check_memcache) {
plan tests => 103;
}
else {
plan skip_all => "Memcache configured but not active.";
}
use XML::Atom::Client;
use XML::Atom::Entry;
use XML::Atom::Category;
use XML::Atom::Feed;
use DW::Routing;
my $u = temp_user();
my $pass = "foopass";
$u->set_password($pass);
$u->update_self( { status => 'A' } );
my $api = XML::Atom::Client->new( Version => 1 );
my $r;
sub do_request {
my ( $method, $uri, %opts ) = @_;
my $authenticate = delete $opts{authenticate};
my $data = delete $opts{data} || {};
my $remote = delete $opts{remote} || $u;
my $password = delete $opts{password} || $remote->password;
$uri =~ m!https?://([^.]+)!;
my $user_subdomain = $1 eq "www" ? "" : $1;
my %routing_data = ();
$routing_data{username} = $user_subdomain if $user_subdomain;
my $input = delete $data->{input};
$r = routing_request(
$uri,
method => $method,
content => $input,
setup_http_request => sub {
if ($authenticate) {
$api->username( $remote->username );
$api->password($password);
$api->munge_request( $_[0] );
}
},
setup_dw_request => sub {
$_[0]->pnote( $_ => $data->{$_} ) foreach %$data;
},
routing_data => \%routing_data,
);
}
# check subject, etc
# items that are not defined in the hash to be checked against are ignored
sub check_entry {
my ( $atom_entry, $entry_info, $journal ) = @_;
local $Test::Builder::Level = $Test::Builder::Level + 1;
subtest "check_entry $entry_info->{id} - $entry_info->{title}" => sub {
ok( $atom_entry, "Got an atom entry back from the server" );
is( $atom_entry->title, $entry_info->{title}, "atom entry has right title" )
if defined $entry_info->{title};
# having the content body be of type HTML
# causes newlines to appear for some reason when we try to extract the content as a string
# so let's just work around it; it should be harmless (as_xml doesn't contain the extra newlines)
my $event_raw = $entry_info->{content};
like( $atom_entry->content->body, qr/\s*$event_raw\s*/, "atom entry has right content" )
if defined $entry_info->{content};
is( $atom_entry->id, $entry_info->{atom_id}, "atom id" )
if defined $entry_info->{atom_id};
is( $atom_entry->author->name, $entry_info->{author}, "atom entry author" )
if defined $entry_info->{author};
if ( defined $entry_info->{url} ) {
my @links = $atom_entry->link;
is( scalar @links, 2, "got back two links" );
foreach my $link (@links) {
if ( $link->rel eq "edit" ) {
is( $link->href, $journal->atom_base . "/entries/$entry_info->{id}",
"edit link" );
}
else { # alternate
is( $link->href, $entry_info->{url}, "entry link" );
}
}
}
if ( defined $entry_info->{categories} ) {
my %tags = map { $_ => 1 } @{ $entry_info->{categories} || [] };
my %categories = map { $_->term => 1 } $atom_entry->category;
is( scalar keys %categories, 2, "got back multiple categories" );
is_deeply( {%categories}, {%tags}, "got back the categories we sent in" );
}
};
}
note("Authentication");
do_request( GET => $u->atom_service_document );
is( $r->status, $r->HTTP_UNAUTHORIZED, "Did not pass any authorization information." );
is( $r->content_type, "text/plain", "Error content type" );
# intentionally break authorization
do_request( GET => $u->atom_service_document, authenticate => 1, password => $u->password x 3 );
is( $r->status, $r->HTTP_UNAUTHORIZED, "Passed wrong authorization information." );
do_request( GET => $u->atom_service_document, authenticate => 1 );
is( $r->status, $r->HTTP_OK, "Successful authentication." );
note("Service document introspection.");
do_request( POST => $u->atom_service_document );
is( $r->status, $r->HTTP_METHOD_NOT_ALLOWED, "Service document needs GET (even unauthorized)" );
do_request( POST => $u->atom_service_document, authenticate => 1 );
is( $r->status, $r->HTTP_METHOD_NOT_ALLOWED, "Service document needs GET." );
do_request( GET => $u->atom_service_document, authenticate => 1 );
is( $r->status, $r->HTTP_OK, "Got service document." );
like( $r->content_type, qr#^\Qapplication/atomsvc+xml\E#, "Service content type" );
my $service_document_xml = $r->response_content;
note("Categories document.");
# populate journal with some tags
my @journal_tags = qw( a b c );
LJ::Tags::create_usertag( $u, join( ", ", @journal_tags ), { display => 1 } );
do_request( GET => $u->atom_base . "/entries/tags" );
is( $r->status, $r->HTTP_UNAUTHORIZED, "Categories document protected by authorization." );
do_request( POST => $u->atom_base . "/entries/tags", authenticate => 1 );
is( $r->status, $r->HTTP_METHOD_NOT_ALLOWED, "Categories document needs GET." );
do_request( GET => $u->atom_base . "/entries/tags", authenticate => 1 );
is( $r->status, $r->HTTP_OK, "Got categories document." );
like( $r->content_type, qr#^\Qapplication/atomcat+xml\E#, "Categories document type" );
my $categories_document_xml = $r->response_content;
SKIP: {
skip "No XML::Atom::Service/XML::Atom::Categories module installed.", 10
unless eval "use XML::Atom::Service; use XML::Atom::Categories; 1;";
my $service = XML::Atom::Service->new( \$service_document_xml );
ok( $service, "Got service document." );
my @workspaces = $service->workspace;
is( scalar @workspaces, 1, "One workspace" );
is( $workspaces[0]->title, $u->user, "Workspace title" );
my @collections = $workspaces[0]->collections;
is( scalar @collections, 1, "One collection" );
is( $collections[0]->title, "Entries", "Entries collection title" );
is( $collections[0]->href, $u->atom_base . "/entries", "Entries collection uri" );
my @categories = $collections[0]->categories;
is( scalar @categories, 1, "One categories link" );
is( $categories[0]->href, $u->atom_base . "/entries/tags", "Categories collection uri" );
my $categories = XML::Atom::Categories->new( \$categories_document_xml );
@categories = $categories->category;
is( scalar @categories, 3, "three existing categories" );
is_deeply(
{ map { $_->term => 1 } @categories },
{ map { $_ => 1 } @journal_tags },
"Journal tags match fetched categories"
);
}
my $atom_entry; # an XML::Atom::Entry object
my $entry_obj; # an LJ::Entry object
my $atom_entry_server; # an XML::Atom::Entry object retrieved from the server
$atom_entry = XML::Atom::Entry->new( Version => 1 );
my $title = "New Post";
my $content = "Content of my post at " . rand();
my @tags = qw( foo bar );
$atom_entry->title($title);
$atom_entry->content($content);
foreach my $tag (@tags) {
my $category = XML::Atom::Category->new( Version => 1 );
$category->term($tag);
$atom_entry->add_category($category);
}
note("Create an entry.");
do_request( POST => $u->atom_base . "/entries", data => { input => $atom_entry->as_xml } );
is( $r->status, $r->HTTP_UNAUTHORIZED, "Entry creation protected by authorization." );
do_request(
POST => $u->atom_base . "/entries",
authenticate => 1,
data => { input => $atom_entry->as_xml }
);
is( $r->status, $r->HTTP_CREATED, "POSTed new entry" );
is( $r->content_type, "application/atom+xml", "AtomAPI entry content type" );
note("Double-check posted entry.");
$entry_obj = LJ::Entry->new( $u, jitemid => 1 );
ok( $entry_obj, "got entry" );
ok( $entry_obj->valid, "entry is valid" );
is( $entry_obj->subject_raw, $title, "item has right title" );
is( $entry_obj->event_raw, $content, "item has right content" );
$atom_entry_server = XML::Atom::Entry->new( \$r->response_content );
check_entry(
$atom_entry_server,
{
id => $entry_obj->jitemid,
title => $atom_entry_server->title,
content => $atom_entry_server->content->body,
url => $entry_obj->url,
author => $u->name_orig,
categories => \@tags,
},
$u
);
ok( $atom_entry_server->published eq $atom_entry_server->updated, "same publish and edit date" );
ok( !$atom_entry_server->summary, "no summary; we have the content." );
note("List entries");
do_request( GET => $u->atom_base . "/entries" );
is( $r->status, $r->HTTP_UNAUTHORIZED, "Entries feed needs authorization." );
do_request( GET => $u->atom_base . "/entries", authenticate => 1 );
is( $r->status, $r->HTTP_OK, "Retrieved entry list" );
is( $r->content_type, "application/atom+xml", "AtomAPI entry content type" );
my $feed = XML::Atom::Feed->new( \$r->response_content );
my @entries = $feed->entries;
is( scalar @entries, 1, "Got entry from feed." );
note("Retrieve entry");
do_request( GET => $u->atom_base . "/entries/1" );
is( $r->status, $r->HTTP_UNAUTHORIZED, "Retrieving entry needs authorization." );
do_request( GET => $u->atom_base . "/entries/12345", authenticate => 1 );
is( $r->status, $r->NOT_FOUND, "No such entry" );
is( $r->content_type, "text/plain", "AtomAPI entry content type" );
do_request( POST => $u->atom_base . "/entries/1", authenticate => 1 );
is( $r->status, $r->HTTP_METHOD_NOT_ALLOWED, $u->atom_base . "/entries/1 does not support POST." );
do_request( GET => $u->atom_base . "/entries/1", authenticate => 1 );
is( $r->status, $r->HTTP_OK, "Retrieved entry" );
is( $r->content_type, "application/atom+xml", "AtomAPI entry content type" );
$atom_entry_server = XML::Atom::Entry->new( \$r->response_content );
check_entry(
$atom_entry_server,
{
id => $entry_obj->jitemid,
title => $entry_obj->subject_raw,
content => $entry_obj->event_raw,
atom_id => $entry_obj->atom_id,
url => $entry_obj->url,
author => $u->name_orig,
categories => \@tags
},
$u
);
ok( $atom_entry_server->published eq $atom_entry_server->updated, "same publish and edit date" );
ok( !$atom_entry_server->summary, "no summary; we have the content." );
note("Edit entry");
do_request( PUT => $u->atom_base . "/entries/1" );
is( $r->status, $r->HTTP_UNAUTHORIZED, "Retrieving entry needs authorization." );
do_request( PUT => $u->atom_base . "/entries/12345", authenticate => 1 );
is( $r->status, $r->NOT_FOUND, "No such entry" );
is( $r->content_type, "text/plain", "AtomAPI entry content type" );
$atom_entry = XML::Atom::Entry->new( Version => 1 );
$title = "Edited Post";
$content = "Content of my post at " . rand();
@tags = qw( foo2 bar2 );
$atom_entry->id( $atom_entry_server->id );
$atom_entry->title($title);
$atom_entry->content($content);
foreach my $tag (@tags) {
my $category = XML::Atom::Category->new( Version => 1 );
$category->term($tag);
$atom_entry->add_category($category);
}
# put a little bit of time between publish and update
sleep(1);
do_request(
PUT => $u->atom_base . "/entries/1",
authenticate => 1,
data => { input => $atom_entry->as_xml }
);
is( $r->status, $r->HTTP_OK, "Edited entry" );
is( $r->content_type, "application/atom+xml", "AtomAPI entry content type" );
do_request( GET => $u->atom_base . "/entries/1", authenticate => 1 );
$atom_entry_server = XML::Atom::Entry->new( \$r->response_content );
check_entry(
$atom_entry_server,
{
id => $entry_obj->jitemid,
title => $title,
content => $content,
atom_id => $entry_obj->atom_id,
url => $entry_obj->url,
author => $u->name_orig,
categories => \@tags
},
$u
);
ok( $atom_entry_server->published ne $atom_entry_server->updated,
"different publish and edit date" );
$atom_entry_server->id("123");
do_request(
PUT => $u->atom_base . "/entries/1",
authenticate => 1,
data => { input => $atom_entry_server->as_xml }
);
is( $r->status, $r->HTTP_BAD_REQUEST, "Mismatched ids" );
do_request( DELETE => $u->atom_base . "/entries/1", authenticate => 1 );
is( $r->status, $r->HTTP_OK, "Deleted entry" );
$entry_obj = LJ::Entry->new( $u, jitemid => 1 );
isnt( $entry_obj->valid, "Entry confirmed deleted" );
do_request( PUT => $u->atom_base . "/entries/1", authenticate => 1 );
is( $r->status, $r->NOT_FOUND, "Trying to edit deleted entry" );
note("Checking community functionality.");
{
my $memberof_cu = temp_comm();
my $nonmemberof_cu = temp_comm();
$u->join_community( $memberof_cu, 1, 1 );
my $another_u = temp_user(); # another member of the community
$another_u->set_password($pass);
$another_u->join_community( $memberof_cu, 1, 1 );
my $admin_u = temp_user(); # an administrator of the community
$admin_u->set_password($pass);
$admin_u->join_community( $memberof_cu, 1, 1 );
LJ::set_rel( $memberof_cu->userid, $admin_u->userid, "A" );
note("Service document introspection (community).");
# unauthenticated to community
do_request( GET => $memberof_cu->atom_service_document );
is( $r->status, $r->HTTP_UNAUTHORIZED, "Service document protected by authorization." );
# community you aren't a member of
do_request( GET => $nonmemberof_cu->atom_service_document, authenticate => 1 );
is( $r->status, $r->HTTP_OK,
"Not a member of the community, but we still get the service document for the user (which doesn't contain the community)."
);
SKIP: {
skip "No XML::Atom::Service/XML::Atom::Categories module installed.", 3
unless eval "use XML::Atom::Service; use XML::Atom::Categories; 1;";
my $service_document_xml = $r->response_content;
my $service = XML::Atom::Service->new( \$service_document_xml );
ok( $service, "Got service document." );
my @workspaces = $service->workspace;
is( scalar @workspaces, 2, "One workspace" );
isnt( $_->title, $nonmemberof_cu->user,
"Community you're not a member of doesn't appear in the service document." )
foreach @workspaces;
}
# community you are a member of
do_request( GET => $memberof_cu->atom_service_document, authenticate => 1 );
is( $r->status, $r->HTTP_OK, "Got service document." );
like( $r->content_type, qr#^\Qapplication/atomsvc+xml\E#, "Service content type" );
SKIP: {
skip "No XML::Atom::Service/XML::Atom::Categories module installed.", 8
unless eval "use XML::Atom::Service; use XML::Atom::Categories; 1;";
my $service_document_xml = $r->response_content;
my $service = XML::Atom::Service->new( \$service_document_xml );
ok( $service, "Got service document." );
my @workspaces = $service->workspace;
is( scalar @workspaces, 2, "Personal journal and community as separate workspaces" );
# making assumptions that the second workspace is our community
my $memberof_cu_workspace = $workspaces[1];
is( $memberof_cu_workspace->title, $memberof_cu->user, "Workspace title" );
my @collections = $memberof_cu_workspace->collections;
is( scalar @collections, 1, "One collection" );
is( $collections[0]->title, "Entries", "Entries collection title" );
is( $collections[0]->href, $memberof_cu->atom_base . "/entries", "Entries collection uri" );
my @categories = $collections[0]->categories;
is( scalar @categories, 1, "One categories link" );
is(
$categories[0]->href,
$memberof_cu->atom_base . "/entries/tags",
"Categories collection uri"
);
}
note("Create an entry (community).");
my $title = "Community entry";
my $content = "Community entry content " . rand();
my $atom_entry = XML::Atom::Entry->new( Version => 1 );
$atom_entry->title($title);
$atom_entry->content($content);
# unauthenticated to community
do_request(
POST => $memberof_cu->atom_base . "/entries",
data => { input => $atom_entry->as_xml }
);
is( $r->status, $r->HTTP_UNAUTHORIZED, "Trying to post to community while unauthenticated." );
# community you don't have posting access to
do_request(
POST => $nonmemberof_cu->atom_base . "/entries",
authenticate => 1,
data => { input => $atom_entry->as_xml }
);
is( $r->status, $r->HTTP_UNAUTHORIZED,
"Trying to post to community, but don't have posting access." );
# community you have posting access to
do_request(
POST => $memberof_cu->atom_base . "/entries",
authenticate => 1,
data => { input => $atom_entry->as_xml }
);
is( $r->status, $r->HTTP_CREATED, "POSTed new entry" );
is( $r->content_type, "application/atom+xml", "AtomAPI entry content type" );
note("Double-check posted entry (community).");
$entry_obj = LJ::Entry->new( $memberof_cu, jitemid => 1 );
ok( $entry_obj, "got entry" );
ok( $entry_obj->valid, "entry is valid" );
is( $entry_obj->subject_raw, $title, "item has right title" );
is( $entry_obj->event_raw, $content, "item has right content" );
$atom_entry_server = XML::Atom::Entry->new( \$r->response_content );
check_entry(
$atom_entry_server,
{
id => $entry_obj->jitemid,
title => $atom_entry_server->title,
content => $atom_entry_server->content->body,
url => $entry_obj->url,
author => $u->name_orig,
},
$memberof_cu
);
note("List entries (community).");
# unauthenticated to community
do_request( GET => $memberof_cu->atom_base . "/entries" );
is( $r->status, $r->HTTP_UNAUTHORIZED, "Entries feed needs authorization." );
# community you don't have posting access to
do_request( GET => $nonmemberof_cu->atom_base . "/entries", authenticate => 1 );
is( $r->status, $r->HTTP_UNAUTHORIZED, "Entries feed needs authorization." );
# community you have posting access to
do_request( GET => $memberof_cu->atom_base . "/entries", authenticate => 1 );
is( $r->status, $r->HTTP_OK, "Retrieved entry list" );
is( $r->content_type, "application/atom+xml", "AtomAPI entry content type" );
my $feed = XML::Atom::Feed->new( \$r->response_content );
my @entries = $feed->entries;
is( scalar @entries, 1, "Got entry from feed." );
note("Retrieve entry (community)");
# unauthenticated to community
do_request( GET => $memberof_cu->atom_base . "/entries/1" );
is( $r->status, $r->HTTP_UNAUTHORIZED, "Retrieving entry needs authorization." );
# community you don't have posting access to
do_request( GET => $nonmemberof_cu->atom_base . "/entries/1", authenticate => 1 );
is( $r->status, $r->HTTP_UNAUTHORIZED, "Retrieving entry needs authorization." );
# community you have posting access to
# retrieve (should succeed)
# edit (should succeed)
# delete (should succeed)
do_request( GET => $memberof_cu->atom_base . "/entries/1", authenticate => 1 );
is( $r->status, $r->HTTP_OK, "Retrieved entry" );
is( $r->content_type, "application/atom+xml", "AtomAPI entry content type" );
$atom_entry_server = XML::Atom::Entry->new( \$r->response_content );
check_entry(
$atom_entry_server,
{
id => $entry_obj->jitemid,
title => $entry_obj->subject_raw,
content => $entry_obj->event_raw,
atom_id => $entry_obj->atom_id,
url => $entry_obj->url,
author => $u->name_orig
},
$memberof_cu
);
$atom_entry = XML::Atom::Entry->new( Version => 1 );
$title = "Edited Post";
$content = "Content of my post at " . rand();
$atom_entry->id( $atom_entry_server->id );
$atom_entry->title($title);
$atom_entry->content($content);
do_request(
PUT => $memberof_cu->atom_base . "/entries/1",
authenticate => 1,
data => { input => $atom_entry->as_xml }
);
is( $r->status, $r->HTTP_OK, "Edited entry" );
is( $r->content_type, "application/atom+xml", "AtomAPI entry content type" );
do_request( GET => $memberof_cu->atom_base . "/entries/1", authenticate => 1 );
$atom_entry_server = XML::Atom::Entry->new( \$r->response_content );
check_entry(
$atom_entry_server,
{
id => $entry_obj->jitemid,
title => $title,
content => $content,
atom_id => $entry_obj->atom_id,
url => $entry_obj->url,
author => $u->name_orig,
},
$memberof_cu
);
do_request( DELETE => $memberof_cu->atom_base . "/entries/1", authenticate => 1 );
is( $r->status, $r->HTTP_OK, "Deleted entry" );
$entry_obj = LJ::Entry->new( $memberof_cu, jitemid => 1 );
isnt( $entry_obj->valid, "Entry confirmed deleted" );
note("Check what other people can do.");
# make another entry that other people can view/manipulate
do_request(
POST => $memberof_cu->atom_base . "/entries",
authenticate => 1,
data => { input => $atom_entry->as_xml }
);
$atom_entry_server = XML::Atom::Entry->new( \$r->response_content );
$atom_entry = XML::Atom::Entry->new( Version => 1 );
$title = "Edited Post";
$content = "Content of my post at " . rand();
$atom_entry->id( $atom_entry_server->id );
$atom_entry->title($title);
$atom_entry->content($content);
# another community member
# retrieve (should fail)
# edit (should fail)
# delete (should fail)
do_request(
GET => $memberof_cu->atom_base . "/entries/2",
authenticate => 1,
remote => $another_u
);
is( $r->status, $r->HTTP_UNAUTHORIZED, "You don't own this entry (another_u, get)" );
do_request(
PUT => $memberof_cu->atom_base . "/entries/2",
authenticate => 1,
remote => $another_u,
data => { input => $atom_entry->as_xml }
);
is( $r->status, $r->HTTP_UNAUTHORIZED, "You don't own this entry (another_u, edit)" );
do_request(
DELETE => $memberof_cu->atom_base . "/entries/2",
authenticate => 1,
remote => $another_u
);
is( $r->status, $r->HTTP_UNAUTHORIZED, "You don't own this entry (another_u, delete)" );
# community admin
# retrieve (should succeed)
# edit (should fail)
# delete (should succeed)
do_request(
GET => $memberof_cu->atom_base . "/entries/2",
authenticate => 1,
remote => $admin_u
);
is( $r->status, $r->HTTP_OK, "Retrieved entry" );
is( $r->content_type, "application/atom+xml", "AtomAPI entry content type" );
do_request(
PUT => $memberof_cu->atom_base . "/entries/2",
authenticate => 1,
remote => $admin_u,
data => { input => $atom_entry->as_xml }
);
is( $r->status, $r->HTTP_UNAUTHORIZED, "You don't own this entry (admin_u, edit)" );
do_request(
DELETE => $memberof_cu->atom_base . "/entries/2",
authenticate => 1,
remote => $admin_u
);
is( $r->status, $r->HTTP_OK, "Deleted entry (admin_u, delete)" );
$entry_obj = LJ::Entry->new( $memberof_cu, jitemid => 2 );
isnt( $entry_obj->valid, "Entry confirmed deleted" );
}
done_testing();