1
// Copyright 2014 Canonical Ltd.
2
// Licensed under the AGPLv3, see LICENCE file for details.
4
package v4_test // import "gopkg.in/juju/charmstore.v5-unstable/internal/v4"
12
jc "github.com/juju/testing/checkers"
13
"github.com/juju/testing/httptesting"
14
gc "gopkg.in/check.v1"
15
"gopkg.in/juju/charm.v6-unstable"
16
"gopkg.in/juju/charmrepo.v2-unstable/csclient/params"
18
"gopkg.in/juju/charmstore.v5-unstable/internal/mongodoc"
21
type logSuite struct {
25
var _ = gc.Suite(&logSuite{})
27
func (s *logSuite) SetUpSuite(c *gc.C) {
28
s.enableIdentity = true
29
s.commonSuite.SetUpSuite(c)
32
var logResponses = map[string]*params.LogResponse{
34
Data: rawMessage("info data 1"),
35
Level: params.InfoLevel,
36
Type: params.IngestionType,
39
Data: rawMessage("error data 1"),
40
Level: params.ErrorLevel,
41
Type: params.IngestionType,
44
Data: rawMessage("info data 2"),
45
Level: params.InfoLevel,
46
Type: params.IngestionType,
48
charm.MustParseURL("precise/django"),
49
charm.MustParseURL("django"),
50
charm.MustParseURL("rails"),
54
Data: rawMessage("warning data 1"),
55
Level: params.WarningLevel,
56
Type: params.IngestionType,
59
Data: rawMessage("error data 2"),
60
Level: params.ErrorLevel,
61
Type: params.IngestionType,
63
charm.MustParseURL("hadoop"),
67
Data: rawMessage("info data 3"),
68
Level: params.InfoLevel,
69
Type: params.IngestionType,
71
charm.MustParseURL("trusty/django"),
72
charm.MustParseURL("django"),
73
charm.MustParseURL("utopic/hadoop"),
74
charm.MustParseURL("hadoop"),
78
Data: rawMessage("error data 3"),
79
Level: params.ErrorLevel,
80
Type: params.IngestionType,
82
charm.MustParseURL("utopic/hadoop"),
83
charm.MustParseURL("hadoop"),
84
charm.MustParseURL("precise/django"),
85
charm.MustParseURL("django"),
89
Data: rawMessage("statistics info data"),
90
Level: params.InfoLevel,
91
Type: params.LegacyStatisticsType,
95
var getLogsTests = []struct {
98
expectBody []*params.LogResponse
100
about: "retrieve logs",
101
expectBody: []*params.LogResponse{
102
logResponses["stats"],
103
logResponses["error3"],
104
logResponses["info3"],
105
logResponses["error2"],
106
logResponses["warning1"],
107
logResponses["info2"],
108
logResponses["error1"],
109
logResponses["info1"],
113
querystring: "?limit=2",
114
expectBody: []*params.LogResponse{
115
logResponses["stats"],
116
logResponses["error3"],
120
querystring: "?skip=3",
121
expectBody: []*params.LogResponse{
122
logResponses["error2"],
123
logResponses["warning1"],
124
logResponses["info2"],
125
logResponses["error1"],
126
logResponses["info1"],
129
about: "zero offset",
130
querystring: "?skip=0",
131
expectBody: []*params.LogResponse{
132
logResponses["stats"],
133
logResponses["error3"],
134
logResponses["info3"],
135
logResponses["error2"],
136
logResponses["warning1"],
137
logResponses["info2"],
138
logResponses["error1"],
139
logResponses["info1"],
142
about: "use both limit and offset",
143
querystring: "?limit=3&skip=1",
144
expectBody: []*params.LogResponse{
145
logResponses["error3"],
146
logResponses["info3"],
147
logResponses["error2"],
150
about: "filter by level",
151
querystring: "?level=info",
152
expectBody: []*params.LogResponse{
153
logResponses["stats"],
154
logResponses["info3"],
155
logResponses["info2"],
156
logResponses["info1"],
159
about: "filter by type",
160
querystring: "?type=ingestion",
161
expectBody: []*params.LogResponse{
162
logResponses["error3"],
163
logResponses["info3"],
164
logResponses["error2"],
165
logResponses["warning1"],
166
logResponses["info2"],
167
logResponses["error1"],
168
logResponses["info1"],
171
about: "filter by level with a limit",
172
querystring: "?level=error&limit=2",
173
expectBody: []*params.LogResponse{
174
logResponses["error3"],
175
logResponses["error2"],
178
about: "filter by id",
179
querystring: "?id=precise/django",
180
expectBody: []*params.LogResponse{
181
logResponses["error3"],
182
logResponses["info2"],
185
about: "multiple query",
186
querystring: "?id=utopic/hadoop&limit=1&level=error",
187
expectBody: []*params.LogResponse{
188
logResponses["error3"],
191
about: "empty response offset",
192
querystring: "?id=utopic/hadoop&skip=10",
194
about: "empty response id not found",
195
querystring: "?id=utopic/mysql",
197
about: "empty response level",
198
querystring: "?id=trusty/rails&level=error",
200
about: "filter by type - legacyStatistics",
201
querystring: "?type=legacyStatistics",
202
expectBody: []*params.LogResponse{
203
logResponses["stats"],
207
var paramsLogLevels = map[params.LogLevel]mongodoc.LogLevel{
208
params.InfoLevel: mongodoc.InfoLevel,
209
params.WarningLevel: mongodoc.WarningLevel,
210
params.ErrorLevel: mongodoc.ErrorLevel,
213
// paramsLogTypes maps API params log types to internal mongodoc ones.
214
var paramsLogTypes = map[params.LogType]mongodoc.LogType{
215
params.IngestionType: mongodoc.IngestionType,
216
params.LegacyStatisticsType: mongodoc.LegacyStatisticsType,
219
func (s *logSuite) TestGetLogs(c *gc.C) {
220
// Add logs to the database.
221
beforeAdding := time.Now().Add(-time.Second)
222
for _, key := range []string{"info1", "error1", "info2", "warning1", "error2", "info3", "error3", "stats"} {
223
resp := logResponses[key]
224
err := s.store.AddLog(&resp.Data, paramsLogLevels[resp.Level], paramsLogTypes[resp.Type], resp.URLs)
225
c.Assert(err, gc.IsNil)
227
afterAdding := time.Now().Add(time.Second)
230
for i, test := range getLogsTests {
231
c.Logf("test %d: %s", i, test.about)
232
rec := httptesting.DoRequest(c, httptesting.DoRequestParams{
234
URL: storeURL("log" + test.querystring),
235
Username: testUsername,
236
Password: testPassword,
239
// Ensure the response is what we expect.
240
c.Assert(rec.Code, gc.Equals, http.StatusOK)
241
c.Assert(rec.Header().Get("Content-Type"), gc.Equals, "application/json")
243
// Decode the response.
244
var logs []*params.LogResponse
245
decoder := json.NewDecoder(rec.Body)
246
err := decoder.Decode(&logs)
247
c.Assert(err, gc.IsNil)
249
// Check and then reset the response time so that the whole body
250
// can be more easily compared later.
251
for _, log := range logs {
252
c.Assert(log.Time, jc.TimeBetween(beforeAdding, afterAdding))
253
log.Time = time.Time{}
256
// Ensure the response includes the expected logs.
257
c.Assert(logs, jc.DeepEquals, test.expectBody)
261
func rawMessage(msg string) json.RawMessage {
262
message, err := json.Marshal(msg)
266
return json.RawMessage(message)
269
var getLogsErrorsTests = []struct {
274
expectCode params.ErrorCode
276
about: "invalid limit (negative number)",
277
querystring: "?limit=-100",
278
expectStatus: http.StatusBadRequest,
279
expectMessage: "invalid limit value: value must be >= 1",
280
expectCode: params.ErrBadRequest,
282
about: "invalid limit (zero value)",
283
querystring: "?limit=0",
284
expectStatus: http.StatusBadRequest,
285
expectMessage: "invalid limit value: value must be >= 1",
286
expectCode: params.ErrBadRequest,
288
about: "invalid limit (not a number)",
289
querystring: "?limit=foo",
290
expectStatus: http.StatusBadRequest,
291
expectMessage: "invalid limit value: value must be a number",
292
expectCode: params.ErrBadRequest,
294
about: "invalid offset (negative number)",
295
querystring: "?skip=-100",
296
expectStatus: http.StatusBadRequest,
297
expectMessage: "invalid skip value: value must be >= 0",
298
expectCode: params.ErrBadRequest,
300
about: "invalid offset (not a number)",
301
querystring: "?skip=bar",
302
expectStatus: http.StatusBadRequest,
303
expectMessage: "invalid skip value: value must be a number",
304
expectCode: params.ErrBadRequest,
307
querystring: "?id=no-such:reference",
308
expectStatus: http.StatusBadRequest,
309
expectMessage: `invalid id value: charm or bundle URL has invalid schema: "no-such:reference"`,
310
expectCode: params.ErrBadRequest,
312
about: "invalid log level",
313
querystring: "?level=bar",
314
expectStatus: http.StatusBadRequest,
315
expectMessage: "invalid log level value",
316
expectCode: params.ErrBadRequest,
318
about: "invalid log type",
319
querystring: "?type=no-such",
320
expectStatus: http.StatusBadRequest,
321
expectMessage: "invalid log type value",
322
expectCode: params.ErrBadRequest,
325
func (s *logSuite) TestGetLogsErrors(c *gc.C) {
326
for i, test := range getLogsErrorsTests {
327
c.Logf("test %d: %s", i, test.about)
328
httptesting.AssertJSONCall(c, httptesting.JSONCallParams{
330
URL: storeURL("log" + test.querystring),
331
Username: testUsername,
332
Password: testPassword,
333
ExpectStatus: test.expectStatus,
334
ExpectBody: params.Error{
335
Message: test.expectMessage,
336
Code: test.expectCode,
342
func (s *logSuite) TestGetLogsErrorInvalidLog(c *gc.C) {
343
// Add a non-parsable log message to the db directly.
344
err := s.store.DB.Logs().Insert(mongodoc.Log{
346
Level: mongodoc.InfoLevel,
347
Type: mongodoc.IngestionType,
350
c.Assert(err, gc.IsNil)
351
// The log is just ignored.
352
httptesting.AssertJSONCall(c, httptesting.JSONCallParams{
354
URL: storeURL("log"),
355
Username: testUsername,
356
Password: testPassword,
357
ExpectStatus: http.StatusOK,
358
ExpectBody: []params.LogResponse{},
362
func (s *logSuite) TestPostLogs(c *gc.C) {
363
// Prepare the request body.
364
body := makeByteLogs(rawMessage("info data"), params.InfoLevel, params.IngestionType, []*charm.URL{
365
charm.MustParseURL("trusty/django"),
366
charm.MustParseURL("utopic/rails"),
370
httptesting.AssertJSONCall(c, httptesting.JSONCallParams{
372
URL: storeURL("log"),
374
Username: testUsername,
375
Password: testPassword,
377
"Content-Type": {"application/json"},
379
Body: bytes.NewReader(body),
380
ExpectStatus: http.StatusOK,
383
// Ensure the log message has been added to the database.
385
err := s.store.DB.Logs().Find(nil).One(&doc)
386
c.Assert(err, gc.IsNil)
387
c.Assert(string(doc.Data), gc.Equals, `"info data"`)
388
c.Assert(doc.Level, gc.Equals, mongodoc.InfoLevel)
389
c.Assert(doc.Type, gc.Equals, mongodoc.IngestionType)
390
c.Assert(doc.URLs, jc.DeepEquals, []*charm.URL{
391
charm.MustParseURL("trusty/django"),
392
charm.MustParseURL("django"),
393
charm.MustParseURL("utopic/rails"),
394
charm.MustParseURL("rails"),
398
func (s *logSuite) TestPostLogsMultipleEntries(c *gc.C) {
399
// Prepare the request body.
400
infoData := rawMessage("info data")
401
warningData := rawMessage("warning data")
402
logs := []params.Log{{
404
Level: params.InfoLevel,
405
Type: params.IngestionType,
408
Level: params.WarningLevel,
409
Type: params.IngestionType,
411
body, err := json.Marshal(logs)
412
c.Assert(err, gc.IsNil)
415
httptesting.AssertJSONCall(c, httptesting.JSONCallParams{
417
URL: storeURL("log"),
419
Username: testUsername,
420
Password: testPassword,
422
"Content-Type": {"application/json"},
424
Body: bytes.NewReader(body),
425
ExpectStatus: http.StatusOK,
428
// Ensure the log messages has been added to the database.
429
var docs []mongodoc.Log
430
err = s.store.DB.Logs().Find(nil).Sort("id").All(&docs)
431
c.Assert(err, gc.IsNil)
432
c.Assert(docs, gc.HasLen, 2)
433
c.Assert(string(docs[0].Data), gc.Equals, string(infoData))
434
c.Assert(docs[0].Level, gc.Equals, mongodoc.InfoLevel)
435
c.Assert(string(docs[1].Data), gc.Equals, string(warningData))
436
c.Assert(docs[1].Level, gc.Equals, mongodoc.WarningLevel)
439
var postLogsErrorsTests = []struct {
445
expectCode params.ErrorCode
447
about: "invalid content type",
448
contentType: "application/zip",
449
expectStatus: http.StatusBadRequest,
450
expectMessage: `unexpected Content-Type "application/zip"; expected 'application/json'`,
451
expectCode: params.ErrBadRequest,
453
about: "invalid body",
455
expectStatus: http.StatusBadRequest,
456
expectMessage: "cannot unmarshal body: invalid character '!' looking for beginning of value",
457
expectCode: params.ErrBadRequest,
459
about: "invalid log level",
460
body: makeByteLogs(rawMessage("message"), params.LogLevel(42), params.IngestionType, nil),
461
expectStatus: http.StatusBadRequest,
462
expectMessage: "invalid log level",
463
expectCode: params.ErrBadRequest,
465
about: "invalid log type",
466
body: makeByteLogs(rawMessage("message"), params.WarningLevel, params.LogType(42), nil),
467
expectStatus: http.StatusBadRequest,
468
expectMessage: "invalid log type",
469
expectCode: params.ErrBadRequest,
472
func (s *logSuite) TestPostLogsErrors(c *gc.C) {
473
url := storeURL("log")
474
for i, test := range postLogsErrorsTests {
475
c.Logf("test %d: %s", i, test.about)
476
if test.contentType == "" {
477
test.contentType = "application/json"
479
httptesting.AssertJSONCall(c, httptesting.JSONCallParams{
484
"Content-Type": {test.contentType},
486
Body: bytes.NewReader(test.body),
487
Username: testUsername,
488
Password: testPassword,
489
ExpectStatus: test.expectStatus,
490
ExpectBody: params.Error{
491
Message: test.expectMessage,
492
Code: test.expectCode,
498
func (s *logSuite) TestGetLogsUnauthorizedError(c *gc.C) {
499
s.AssertEndpointAuth(c, httptesting.JSONCallParams{
500
URL: storeURL("log"),
501
ExpectStatus: http.StatusOK,
502
ExpectBody: []params.LogResponse{},
506
func (s *logSuite) TestPostLogsUnauthorizedError(c *gc.C) {
507
// Add a non-parsable log message to the db.
508
httptesting.AssertJSONCall(c, httptesting.JSONCallParams{
509
Handler: s.noMacaroonSrv,
510
URL: storeURL("log"),
513
"Content-Type": {"application/json"},
515
ExpectStatus: http.StatusUnauthorized,
516
ExpectBody: params.Error{
517
Message: "authentication failed: missing HTTP auth header",
518
Code: params.ErrUnauthorized,
523
func makeByteLogs(data json.RawMessage, logLevel params.LogLevel, logType params.LogType, urls []*charm.URL) []byte {
524
logs := []params.Log{{
530
b, err := json.Marshal(logs)