Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(51)

Side by Side Diff: utils/tests/pub/pub_lish_test.dart

Issue 11943005: Make integration tests a bit cleaner. (Closed) Base URL: https://dart.googlecode.com/svn/branches/bleeding_edge/dart
Patch Set: Created 7 years, 11 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View unified diff | Download patch | Annotate | Revision Log
« no previous file with comments | « utils/tests/pub/oauth2_test.dart ('k') | utils/tests/pub/pub_uploader_test.dart » ('j') | no next file with comments »
Toggle Intra-line Diffs ('i') | Expand Comments ('e') | Collapse Comments ('c') | Show Comments Hide Comments ('s')
OLDNEW
1 // Copyright (c) 2012, the Dart project authors. Please see the AUTHORS file 1 // Copyright (c) 2012, the Dart project authors. Please see the AUTHORS file
2 // for details. All rights reserved. Use of this source code is governed by a 2 // for details. All rights reserved. Use of this source code is governed by a
3 // BSD-style license that can be found in the LICENSE file. 3 // BSD-style license that can be found in the LICENSE file.
4 4
5 library pub_lish_test; 5 library pub_lish_test;
6 6
7 import 'dart:io'; 7 import 'dart:io';
8 import 'dart:json' as json; 8 import 'dart:json' as json;
9 9
10 import 'test_pub.dart'; 10 import 'test_pub.dart';
11 import 'test_pub.dart';
11 import '../../../pkg/unittest/lib/unittest.dart'; 12 import '../../../pkg/unittest/lib/unittest.dart';
12 import '../../pub/io.dart'; 13 import '../../pub/io.dart';
13 14
14 void handleUploadForm(ScheduledServer server, [Map body]) { 15 void handleUploadForm(ScheduledServer server, [Map body]) {
15 server.handle('GET', '/packages/versions/new.json', (request, response) { 16 server.handle('GET', '/packages/versions/new.json', (request, response) {
16 return server.url.then((url) { 17 return server.url.then((url) {
17 expect(request.headers.value('authorization'), 18 expect(request.headers.value('authorization'),
18 equals('Bearer access token')); 19 equals('Bearer access token'));
19 20
20 if (body == null) { 21 if (body == null) {
(...skipping 21 matching lines...) Expand all
42 response.statusCode = 302; 43 response.statusCode = 302;
43 response.headers.set('location', url.resolve('/create').toString()); 44 response.headers.set('location', url.resolve('/create').toString());
44 response.outputStream.close(); 45 response.outputStream.close();
45 }); 46 });
46 }); 47 });
47 } 48 }
48 49
49 main() { 50 main() {
50 setUp(() => normalPackage.scheduleCreate()); 51 setUp(() => normalPackage.scheduleCreate());
51 52
52 test('archives and uploads a package', () { 53 integration('archives and uploads a package', () {
53 var server = new ScheduledServer(); 54 var server = new ScheduledServer();
54 credentialsFile(server, 'access token').scheduleCreate(); 55 credentialsFile(server, 'access token').scheduleCreate();
55 var pub = startPubLish(server); 56 var pub = startPubLish(server);
56 57
57 confirmPublish(pub); 58 confirmPublish(pub);
58 handleUploadForm(server); 59 handleUploadForm(server);
59 handleUpload(server); 60 handleUpload(server);
60 61
61 server.handle('GET', '/create', (request, response) { 62 server.handle('GET', '/create', (request, response) {
62 response.outputStream.writeString(json.stringify({ 63 response.outputStream.writeString(json.stringify({
63 'success': {'message': 'Package test_pkg 1.0.0 uploaded!'} 64 'success': {'message': 'Package test_pkg 1.0.0 uploaded!'}
64 })); 65 }));
65 response.outputStream.close(); 66 response.outputStream.close();
66 }); 67 });
67 68
68 // TODO(rnystrom): The confirm line is run together with this one because 69 // TODO(rnystrom): The confirm line is run together with this one because
69 // in normal usage, the user will have entered a newline on stdin which 70 // in normal usage, the user will have entered a newline on stdin which
70 // gets echoed to the terminal. Do something better here? 71 // gets echoed to the terminal. Do something better here?
71 expectLater(pub.nextLine(), equals( 72 expectLater(pub.nextLine(), equals(
72 'Looks great! Are you ready to upload your package (y/n)?' 73 'Looks great! Are you ready to upload your package (y/n)?'
73 ' Package test_pkg 1.0.0 uploaded!')); 74 ' Package test_pkg 1.0.0 uploaded!'));
74 pub.shouldExit(0); 75 pub.shouldExit(0);
75
76 run();
77 }); 76 });
78 77
79 // TODO(nweiz): Once a multipart/form-data parser in Dart exists, we should 78 // TODO(nweiz): Once a multipart/form-data parser in Dart exists, we should
80 // test that "pub lish" chooses the correct files to publish. 79 // test that "pub lish" chooses the correct files to publish.
81 80
82 test('package validation has an error', () { 81 integration('package validation has an error', () {
83 var package = package("test_pkg", "1.0.0"); 82 var package = package("test_pkg", "1.0.0");
84 package.remove("homepage"); 83 package.remove("homepage");
85 dir(appPath, [pubspec(package)]).scheduleCreate(); 84 dir(appPath, [pubspec(package)]).scheduleCreate();
86 85
87 var server = new ScheduledServer(); 86 var server = new ScheduledServer();
88 var pub = startPubLish(server); 87 var pub = startPubLish(server);
89 88
90 pub.shouldExit(1); 89 pub.shouldExit(1);
91 expectLater(pub.remainingStderr(), 90 expectLater(pub.remainingStderr(),
92 contains("Sorry, your package is missing a requirement and can't be " 91 contains("Sorry, your package is missing a requirement and can't be "
93 "published yet.")); 92 "published yet."));
94
95 run();
96 }); 93 });
97 94
98 test('package validation has a warning and is canceled', () { 95 integration('package validation has a warning and is canceled', () {
99 var package = package("test_pkg", "1.0.0"); 96 var package = package("test_pkg", "1.0.0");
100 package["author"] = "Nathan Weizenbaum"; 97 package["author"] = "Nathan Weizenbaum";
101 dir(appPath, [pubspec(package)]).scheduleCreate(); 98 dir(appPath, [pubspec(package)]).scheduleCreate();
102 99
103 var server = new ScheduledServer(); 100 var server = new ScheduledServer();
104 var pub = startPubLish(server); 101 var pub = startPubLish(server);
105 102
106 pub.writeLine("n"); 103 pub.writeLine("n");
107 pub.shouldExit(1); 104 pub.shouldExit(1);
108 expectLater(pub.remainingStderr(), contains("Package upload canceled.")); 105 expectLater(pub.remainingStderr(), contains("Package upload canceled."));
109
110 run();
111 }); 106 });
112 107
113 test('package validation has a warning and continues', () { 108 integration('package validation has a warning and continues', () {
114 var package = package("test_pkg", "1.0.0"); 109 var package = package("test_pkg", "1.0.0");
115 package["author"] = "Nathan Weizenbaum"; 110 package["author"] = "Nathan Weizenbaum";
116 dir(appPath, [pubspec(package)]).scheduleCreate(); 111 dir(appPath, [pubspec(package)]).scheduleCreate();
117 112
118 var server = new ScheduledServer(); 113 var server = new ScheduledServer();
119 credentialsFile(server, 'access token').scheduleCreate(); 114 credentialsFile(server, 'access token').scheduleCreate();
120 var pub = startPubLish(server); 115 var pub = startPubLish(server);
121 pub.writeLine("y"); 116 pub.writeLine("y");
122 handleUploadForm(server); 117 handleUploadForm(server);
123 handleUpload(server); 118 handleUpload(server);
124 119
125 server.handle('GET', '/create', (request, response) { 120 server.handle('GET', '/create', (request, response) {
126 response.outputStream.writeString(json.stringify({ 121 response.outputStream.writeString(json.stringify({
127 'success': {'message': 'Package test_pkg 1.0.0 uploaded!'} 122 'success': {'message': 'Package test_pkg 1.0.0 uploaded!'}
128 })); 123 }));
129 response.outputStream.close(); 124 response.outputStream.close();
130 }); 125 });
131 126
132 pub.shouldExit(0); 127 pub.shouldExit(0);
133 expectLater(pub.remainingStdout(), 128 expectLater(pub.remainingStdout(),
134 contains('Package test_pkg 1.0.0 uploaded!')); 129 contains('Package test_pkg 1.0.0 uploaded!'));
135
136 run();
137 }); 130 });
138 131
139 test('upload form provides an error', () { 132 integration('upload form provides an error', () {
140 var server = new ScheduledServer(); 133 var server = new ScheduledServer();
141 credentialsFile(server, 'access token').scheduleCreate(); 134 credentialsFile(server, 'access token').scheduleCreate();
142 var pub = startPubLish(server); 135 var pub = startPubLish(server);
143 136
144 confirmPublish(pub); 137 confirmPublish(pub);
145 138
146 server.handle('GET', '/packages/versions/new.json', (request, response) { 139 server.handle('GET', '/packages/versions/new.json', (request, response) {
147 response.statusCode = 400; 140 response.statusCode = 400;
148 response.outputStream.writeString(json.stringify({ 141 response.outputStream.writeString(json.stringify({
149 'error': {'message': 'your request sucked'} 142 'error': {'message': 'your request sucked'}
150 })); 143 }));
151 response.outputStream.close(); 144 response.outputStream.close();
152 }); 145 });
153 146
154 expectLater(pub.nextErrLine(), equals('your request sucked')); 147 expectLater(pub.nextErrLine(), equals('your request sucked'));
155 pub.shouldExit(1); 148 pub.shouldExit(1);
156
157 run();
158 }); 149 });
159 150
160 test('upload form provides invalid JSON', () { 151 integration('upload form provides invalid JSON', () {
161 var server = new ScheduledServer(); 152 var server = new ScheduledServer();
162 credentialsFile(server, 'access token').scheduleCreate(); 153 credentialsFile(server, 'access token').scheduleCreate();
163 var pub = startPubLish(server); 154 var pub = startPubLish(server);
164 155
165 confirmPublish(pub); 156 confirmPublish(pub);
166 157
167 server.handle('GET', '/packages/versions/new.json', (request, response) { 158 server.handle('GET', '/packages/versions/new.json', (request, response) {
168 response.outputStream.writeString('{not json'); 159 response.outputStream.writeString('{not json');
169 response.outputStream.close(); 160 response.outputStream.close();
170 }); 161 });
171 162
172 expectLater(pub.nextErrLine(), equals('Invalid server response:')); 163 expectLater(pub.nextErrLine(), equals('Invalid server response:'));
173 expectLater(pub.nextErrLine(), equals('{not json')); 164 expectLater(pub.nextErrLine(), equals('{not json'));
174 pub.shouldExit(1); 165 pub.shouldExit(1);
175
176 run();
177 }); 166 });
178 167
179 test('upload form is missing url', () { 168 integration('upload form is missing url', () {
180 var server = new ScheduledServer(); 169 var server = new ScheduledServer();
181 credentialsFile(server, 'access token').scheduleCreate(); 170 credentialsFile(server, 'access token').scheduleCreate();
182 var pub = startPubLish(server); 171 var pub = startPubLish(server);
183 172
184 confirmPublish(pub); 173 confirmPublish(pub);
185 174
186 var body = { 175 var body = {
187 'fields': { 176 'fields': {
188 'field1': 'value1', 177 'field1': 'value1',
189 'field2': 'value2' 178 'field2': 'value2'
190 } 179 }
191 }; 180 };
192 181
193 handleUploadForm(server, body); 182 handleUploadForm(server, body);
194 expectLater(pub.nextErrLine(), equals('Invalid server response:')); 183 expectLater(pub.nextErrLine(), equals('Invalid server response:'));
195 expectLater(pub.nextErrLine(), equals(json.stringify(body))); 184 expectLater(pub.nextErrLine(), equals(json.stringify(body)));
196 pub.shouldExit(1); 185 pub.shouldExit(1);
197
198 run();
199 }); 186 });
200 187
201 test('upload form url is not a string', () { 188 integration('upload form url is not a string', () {
202 var server = new ScheduledServer(); 189 var server = new ScheduledServer();
203 credentialsFile(server, 'access token').scheduleCreate(); 190 credentialsFile(server, 'access token').scheduleCreate();
204 var pub = startPubLish(server); 191 var pub = startPubLish(server);
205 192
206 confirmPublish(pub); 193 confirmPublish(pub);
207 194
208 var body = { 195 var body = {
209 'url': 12, 196 'url': 12,
210 'fields': { 197 'fields': {
211 'field1': 'value1', 198 'field1': 'value1',
212 'field2': 'value2' 199 'field2': 'value2'
213 } 200 }
214 }; 201 };
215 202
216 handleUploadForm(server, body); 203 handleUploadForm(server, body);
217 expectLater(pub.nextErrLine(), equals('Invalid server response:')); 204 expectLater(pub.nextErrLine(), equals('Invalid server response:'));
218 expectLater(pub.nextErrLine(), equals(json.stringify(body))); 205 expectLater(pub.nextErrLine(), equals(json.stringify(body)));
219 pub.shouldExit(1); 206 pub.shouldExit(1);
220
221 run();
222 }); 207 });
223 208
224 test('upload form is missing fields', () { 209 integration('upload form is missing fields', () {
225 var server = new ScheduledServer(); 210 var server = new ScheduledServer();
226 credentialsFile(server, 'access token').scheduleCreate(); 211 credentialsFile(server, 'access token').scheduleCreate();
227 var pub = startPubLish(server); 212 var pub = startPubLish(server);
228 213
229 confirmPublish(pub); 214 confirmPublish(pub);
230 215
231 var body = {'url': 'http://example.com/upload'}; 216 var body = {'url': 'http://example.com/upload'};
232 handleUploadForm(server, body); 217 handleUploadForm(server, body);
233 expectLater(pub.nextErrLine(), equals('Invalid server response:')); 218 expectLater(pub.nextErrLine(), equals('Invalid server response:'));
234 expectLater(pub.nextErrLine(), equals(json.stringify(body))); 219 expectLater(pub.nextErrLine(), equals(json.stringify(body)));
235 pub.shouldExit(1); 220 pub.shouldExit(1);
236
237 run();
238 }); 221 });
239 222
240 test('upload form fields is not a map', () { 223 integration('upload form fields is not a map', () {
241 var server = new ScheduledServer(); 224 var server = new ScheduledServer();
242 credentialsFile(server, 'access token').scheduleCreate(); 225 credentialsFile(server, 'access token').scheduleCreate();
243 var pub = startPubLish(server); 226 var pub = startPubLish(server);
244 227
245 confirmPublish(pub); 228 confirmPublish(pub);
246 229
247 var body = {'url': 'http://example.com/upload', 'fields': 12}; 230 var body = {'url': 'http://example.com/upload', 'fields': 12};
248 handleUploadForm(server, body); 231 handleUploadForm(server, body);
249 expectLater(pub.nextErrLine(), equals('Invalid server response:')); 232 expectLater(pub.nextErrLine(), equals('Invalid server response:'));
250 expectLater(pub.nextErrLine(), equals(json.stringify(body))); 233 expectLater(pub.nextErrLine(), equals(json.stringify(body)));
251 pub.shouldExit(1); 234 pub.shouldExit(1);
252
253 run();
254 }); 235 });
255 236
256 test('upload form fields has a non-string value', () { 237 integration('upload form fields has a non-string value', () {
257 var server = new ScheduledServer(); 238 var server = new ScheduledServer();
258 credentialsFile(server, 'access token').scheduleCreate(); 239 credentialsFile(server, 'access token').scheduleCreate();
259 var pub = startPubLish(server); 240 var pub = startPubLish(server);
260 241
261 confirmPublish(pub); 242 confirmPublish(pub);
262 243
263 var body = { 244 var body = {
264 'url': 'http://example.com/upload', 245 'url': 'http://example.com/upload',
265 'fields': {'field': 12} 246 'fields': {'field': 12}
266 }; 247 };
267 handleUploadForm(server, body); 248 handleUploadForm(server, body);
268 expectLater(pub.nextErrLine(), equals('Invalid server response:')); 249 expectLater(pub.nextErrLine(), equals('Invalid server response:'));
269 expectLater(pub.nextErrLine(), equals(json.stringify(body))); 250 expectLater(pub.nextErrLine(), equals(json.stringify(body)));
270 pub.shouldExit(1); 251 pub.shouldExit(1);
271
272 run();
273 }); 252 });
274 253
275 test('cloud storage upload provides an error', () { 254 integration('cloud storage upload provides an error', () {
276 var server = new ScheduledServer(); 255 var server = new ScheduledServer();
277 credentialsFile(server, 'access token').scheduleCreate(); 256 credentialsFile(server, 'access token').scheduleCreate();
278 var pub = startPubLish(server); 257 var pub = startPubLish(server);
279 258
280 confirmPublish(pub); 259 confirmPublish(pub);
281 handleUploadForm(server); 260 handleUploadForm(server);
282 261
283 server.handle('POST', '/upload', (request, response) { 262 server.handle('POST', '/upload', (request, response) {
284 response.statusCode = 400; 263 response.statusCode = 400;
285 response.headers.contentType = new ContentType('application', 'xml'); 264 response.headers.contentType = new ContentType('application', 'xml');
286 response.outputStream.writeString('<Error><Message>Your request sucked.' 265 response.outputStream.writeString('<Error><Message>Your request sucked.'
287 '</Message></Error>'); 266 '</Message></Error>');
288 response.outputStream.close(); 267 response.outputStream.close();
289 }); 268 });
290 269
291 // TODO(nweiz): This should use the server's error message once the client 270 // TODO(nweiz): This should use the server's error message once the client
292 // can parse the XML. 271 // can parse the XML.
293 expectLater(pub.nextErrLine(), equals('Failed to upload the package.')); 272 expectLater(pub.nextErrLine(), equals('Failed to upload the package.'));
294 pub.shouldExit(1); 273 pub.shouldExit(1);
295
296 run();
297 }); 274 });
298 275
299 test("cloud storage upload doesn't redirect", () { 276 integration("cloud storage upload doesn't redirect", () {
300 var server = new ScheduledServer(); 277 var server = new ScheduledServer();
301 credentialsFile(server, 'access token').scheduleCreate(); 278 credentialsFile(server, 'access token').scheduleCreate();
302 var pub = startPubLish(server); 279 var pub = startPubLish(server);
303 280
304 confirmPublish(pub); 281 confirmPublish(pub);
305 handleUploadForm(server); 282 handleUploadForm(server);
306 283
307 server.handle('POST', '/upload', (request, response) { 284 server.handle('POST', '/upload', (request, response) {
308 // don't set the location header 285 // don't set the location header
309 response.outputStream.close(); 286 response.outputStream.close();
310 }); 287 });
311 288
312 expectLater(pub.nextErrLine(), equals('Failed to upload the package.')); 289 expectLater(pub.nextErrLine(), equals('Failed to upload the package.'));
313 pub.shouldExit(1); 290 pub.shouldExit(1);
314
315 run();
316 }); 291 });
317 292
318 test('package creation provides an error', () { 293 integration('package creation provides an error', () {
319 var server = new ScheduledServer(); 294 var server = new ScheduledServer();
320 credentialsFile(server, 'access token').scheduleCreate(); 295 credentialsFile(server, 'access token').scheduleCreate();
321 var pub = startPubLish(server); 296 var pub = startPubLish(server);
322 297
323 confirmPublish(pub); 298 confirmPublish(pub);
324 handleUploadForm(server); 299 handleUploadForm(server);
325 handleUpload(server); 300 handleUpload(server);
326 301
327 server.handle('GET', '/create', (request, response) { 302 server.handle('GET', '/create', (request, response) {
328 response.statusCode = 400; 303 response.statusCode = 400;
329 response.outputStream.writeString(json.stringify({ 304 response.outputStream.writeString(json.stringify({
330 'error': {'message': 'Your package was too boring.'} 305 'error': {'message': 'Your package was too boring.'}
331 })); 306 }));
332 response.outputStream.close(); 307 response.outputStream.close();
333 }); 308 });
334 309
335 expectLater(pub.nextErrLine(), equals('Your package was too boring.')); 310 expectLater(pub.nextErrLine(), equals('Your package was too boring.'));
336 pub.shouldExit(1); 311 pub.shouldExit(1);
337
338 run();
339 }); 312 });
340 313
341 test('package creation provides invalid JSON', () { 314 integration('package creation provides invalid JSON', () {
342 var server = new ScheduledServer(); 315 var server = new ScheduledServer();
343 credentialsFile(server, 'access token').scheduleCreate(); 316 credentialsFile(server, 'access token').scheduleCreate();
344 var pub = startPubLish(server); 317 var pub = startPubLish(server);
345 318
346 confirmPublish(pub); 319 confirmPublish(pub);
347 handleUploadForm(server); 320 handleUploadForm(server);
348 handleUpload(server); 321 handleUpload(server);
349 322
350 server.handle('GET', '/create', (request, response) { 323 server.handle('GET', '/create', (request, response) {
351 response.outputStream.writeString('{not json'); 324 response.outputStream.writeString('{not json');
352 response.outputStream.close(); 325 response.outputStream.close();
353 }); 326 });
354 327
355 expectLater(pub.nextErrLine(), equals('Invalid server response:')); 328 expectLater(pub.nextErrLine(), equals('Invalid server response:'));
356 expectLater(pub.nextErrLine(), equals('{not json')); 329 expectLater(pub.nextErrLine(), equals('{not json'));
357 pub.shouldExit(1); 330 pub.shouldExit(1);
358
359 run();
360 }); 331 });
361 332
362 test('package creation provides a malformed error', () { 333 integration('package creation provides a malformed error', () {
363 var server = new ScheduledServer(); 334 var server = new ScheduledServer();
364 credentialsFile(server, 'access token').scheduleCreate(); 335 credentialsFile(server, 'access token').scheduleCreate();
365 var pub = startPubLish(server); 336 var pub = startPubLish(server);
366 337
367 confirmPublish(pub); 338 confirmPublish(pub);
368 handleUploadForm(server); 339 handleUploadForm(server);
369 handleUpload(server); 340 handleUpload(server);
370 341
371 var body = {'error': 'Your package was too boring.'}; 342 var body = {'error': 'Your package was too boring.'};
372 server.handle('GET', '/create', (request, response) { 343 server.handle('GET', '/create', (request, response) {
373 response.statusCode = 400; 344 response.statusCode = 400;
374 response.outputStream.writeString(json.stringify(body)); 345 response.outputStream.writeString(json.stringify(body));
375 response.outputStream.close(); 346 response.outputStream.close();
376 }); 347 });
377 348
378 expectLater(pub.nextErrLine(), equals('Invalid server response:')); 349 expectLater(pub.nextErrLine(), equals('Invalid server response:'));
379 expectLater(pub.nextErrLine(), equals(json.stringify(body))); 350 expectLater(pub.nextErrLine(), equals(json.stringify(body)));
380 pub.shouldExit(1); 351 pub.shouldExit(1);
381
382 run();
383 }); 352 });
384 353
385 test('package creation provides a malformed success', () { 354 integration('package creation provides a malformed success', () {
386 var server = new ScheduledServer(); 355 var server = new ScheduledServer();
387 credentialsFile(server, 'access token').scheduleCreate(); 356 credentialsFile(server, 'access token').scheduleCreate();
388 var pub = startPubLish(server); 357 var pub = startPubLish(server);
389 358
390 confirmPublish(pub); 359 confirmPublish(pub);
391 handleUploadForm(server); 360 handleUploadForm(server);
392 handleUpload(server); 361 handleUpload(server);
393 362
394 var body = {'success': 'Your package was awesome.'}; 363 var body = {'success': 'Your package was awesome.'};
395 server.handle('GET', '/create', (request, response) { 364 server.handle('GET', '/create', (request, response) {
396 response.outputStream.writeString(json.stringify(body)); 365 response.outputStream.writeString(json.stringify(body));
397 response.outputStream.close(); 366 response.outputStream.close();
398 }); 367 });
399 368
400 expectLater(pub.nextErrLine(), equals('Invalid server response:')); 369 expectLater(pub.nextErrLine(), equals('Invalid server response:'));
401 expectLater(pub.nextErrLine(), equals(json.stringify(body))); 370 expectLater(pub.nextErrLine(), equals(json.stringify(body)));
402 pub.shouldExit(1); 371 pub.shouldExit(1);
403
404 run();
405 }); 372 });
406 } 373 }
OLDNEW
« no previous file with comments | « utils/tests/pub/oauth2_test.dart ('k') | utils/tests/pub/pub_uploader_test.dart » ('j') | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698