1// Copyright 2016 Google LLC
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//      http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15// TODO(jba): test that OnError is getting called appropriately.
16
17package logadmin
18
19import (
20	"context"
21	"flag"
22	"log"
23	"net/http"
24	"net/url"
25	"os"
26	"testing"
27	"time"
28
29	"cloud.google.com/go/internal/testutil"
30	"cloud.google.com/go/logging"
31	ltesting "cloud.google.com/go/logging/internal/testing"
32	"github.com/golang/protobuf/proto"
33	"github.com/golang/protobuf/ptypes"
34	durpb "github.com/golang/protobuf/ptypes/duration"
35	structpb "github.com/golang/protobuf/ptypes/struct"
36	"github.com/google/go-cmp/cmp/cmpopts"
37	"google.golang.org/api/option"
38	mrpb "google.golang.org/genproto/googleapis/api/monitoredres"
39	audit "google.golang.org/genproto/googleapis/cloud/audit"
40	logtypepb "google.golang.org/genproto/googleapis/logging/type"
41	logpb "google.golang.org/genproto/googleapis/logging/v2"
42	"google.golang.org/grpc"
43)
44
45var (
46	client        *Client
47	testProjectID string
48)
49
50var (
51	// If true, this test is using the production service, not a fake.
52	integrationTest bool
53
54	newClient func(ctx context.Context, projectID string) *Client
55)
56
57func TestMain(m *testing.M) {
58	flag.Parse() // needed for testing.Short()
59	ctx := context.Background()
60	testProjectID = testutil.ProjID()
61	if testProjectID == "" || testing.Short() {
62		integrationTest = false
63		if testProjectID != "" {
64			log.Print("Integration tests skipped in short mode (using fake instead)")
65		}
66		testProjectID = "PROJECT_ID"
67		addr, err := ltesting.NewServer()
68		if err != nil {
69			log.Fatalf("creating fake server: %v", err)
70		}
71		newClient = func(ctx context.Context, projectID string) *Client {
72			conn, err := grpc.Dial(addr, grpc.WithInsecure(), grpc.WithBlock())
73			if err != nil {
74				log.Fatalf("dialing %q: %v", addr, err)
75			}
76			c, err := NewClient(ctx, projectID, option.WithGRPCConn(conn))
77			if err != nil {
78				log.Fatalf("creating client for fake at %q: %v", addr, err)
79			}
80			return c
81		}
82	} else {
83		// TODO(enocom): Delete this once we can get these tests to reliably pass.
84		return
85
86		integrationTest = true
87		ts := testutil.TokenSource(ctx, logging.AdminScope)
88		if ts == nil {
89			log.Fatal("The project key must be set. See CONTRIBUTING.md for details")
90		}
91		log.Printf("running integration tests with project %s", testProjectID)
92		newClient = func(ctx context.Context, projectID string) *Client {
93			c, err := NewClient(ctx, projectID, option.WithTokenSource(ts),
94				option.WithGRPCDialOption(grpc.WithBlock()))
95			if err != nil {
96				log.Fatalf("creating prod client: %v", err)
97			}
98			return c
99		}
100	}
101	client = newClient(ctx, testProjectID)
102	initMetrics(ctx)
103	cleanup := initSinks(ctx)
104	exit := m.Run()
105	cleanup()
106	client.Close()
107	os.Exit(exit)
108}
109
110// EntryIterator and DeleteLog are tested in the logging package.
111
112func TestClientClose(t *testing.T) {
113	c := newClient(context.Background(), testProjectID)
114	if err := c.Close(); err != nil {
115		t.Errorf("want got %v, want nil", err)
116	}
117}
118
119func TestFromLogEntry(t *testing.T) {
120	now := time.Now()
121	res := &mrpb.MonitoredResource{Type: "global"}
122	ts, err := ptypes.TimestampProto(now)
123	if err != nil {
124		t.Fatal(err)
125	}
126	logEntry := logpb.LogEntry{
127		LogName:   "projects/PROJECT_ID/logs/LOG_ID",
128		Resource:  res,
129		Payload:   &logpb.LogEntry_TextPayload{TextPayload: "hello"},
130		Timestamp: ts,
131		Severity:  logtypepb.LogSeverity_INFO,
132		InsertId:  "123",
133		HttpRequest: &logtypepb.HttpRequest{
134			RequestMethod:                  "GET",
135			RequestUrl:                     "http:://example.com/path?q=1",
136			RequestSize:                    100,
137			Status:                         200,
138			ResponseSize:                   25,
139			Latency:                        &durpb.Duration{Seconds: 100},
140			UserAgent:                      "user-agent",
141			RemoteIp:                       "127.0.0.1",
142			Referer:                        "referer",
143			CacheHit:                       true,
144			CacheValidatedWithOriginServer: true,
145		},
146		Labels: map[string]string{
147			"a": "1",
148			"b": "two",
149			"c": "true",
150		},
151		SourceLocation: &logpb.LogEntrySourceLocation{
152			File:     "some_file.go",
153			Line:     1,
154			Function: "someFunction",
155		},
156	}
157	u, err := url.Parse("http:://example.com/path?q=1")
158	if err != nil {
159		t.Fatal(err)
160	}
161	want := &logging.Entry{
162		LogName:   "projects/PROJECT_ID/logs/LOG_ID",
163		Resource:  res,
164		Timestamp: now.In(time.UTC),
165		Severity:  logging.Info,
166		Payload:   "hello",
167		Labels: map[string]string{
168			"a": "1",
169			"b": "two",
170			"c": "true",
171		},
172		InsertID: "123",
173		HTTPRequest: &logging.HTTPRequest{
174			Request: &http.Request{
175				Method: "GET",
176				URL:    u,
177				Header: map[string][]string{
178					"User-Agent": {"user-agent"},
179					"Referer":    {"referer"},
180				},
181			},
182			RequestSize:                    100,
183			Status:                         200,
184			ResponseSize:                   25,
185			Latency:                        100 * time.Second,
186			RemoteIP:                       "127.0.0.1",
187			CacheHit:                       true,
188			CacheValidatedWithOriginServer: true,
189		},
190		SourceLocation: &logpb.LogEntrySourceLocation{
191			File:     "some_file.go",
192			Line:     1,
193			Function: "someFunction",
194		},
195	}
196	got, err := fromLogEntry(&logEntry)
197	if err != nil {
198		t.Fatal(err)
199	}
200	if diff := testutil.Diff(got, want, cmpopts.IgnoreUnexported(http.Request{})); diff != "" {
201		t.Errorf("FullEntry:\n%s", diff)
202	}
203
204	// Proto payload.
205	alog := &audit.AuditLog{
206		ServiceName:  "svc",
207		MethodName:   "method",
208		ResourceName: "shelves/S/books/B",
209	}
210	any, err := ptypes.MarshalAny(alog)
211	if err != nil {
212		t.Fatal(err)
213	}
214	logEntry = logpb.LogEntry{
215		LogName:   "projects/PROJECT_ID/logs/LOG_ID",
216		Resource:  res,
217		Timestamp: ts,
218		Payload:   &logpb.LogEntry_ProtoPayload{ProtoPayload: any},
219	}
220	got, err = fromLogEntry(&logEntry)
221	if err != nil {
222		t.Fatal(err)
223	}
224	if !ltesting.PayloadEqual(got.Payload, alog) {
225		t.Errorf("got %+v, want %+v", got.Payload, alog)
226	}
227
228	// JSON payload.
229	jstruct := &structpb.Struct{Fields: map[string]*structpb.Value{
230		"f": {Kind: &structpb.Value_NumberValue{NumberValue: 3.1}},
231	}}
232	logEntry = logpb.LogEntry{
233		LogName:   "projects/PROJECT_ID/logs/LOG_ID",
234		Resource:  res,
235		Timestamp: ts,
236		Payload:   &logpb.LogEntry_JsonPayload{JsonPayload: jstruct},
237	}
238	got, err = fromLogEntry(&logEntry)
239	if err != nil {
240		t.Fatal(err)
241	}
242	if !ltesting.PayloadEqual(got.Payload, jstruct) {
243		t.Errorf("got %+v, want %+v", got.Payload, jstruct)
244	}
245
246	// No payload.
247	logEntry = logpb.LogEntry{
248		LogName:   "projects/PROJECT_ID/logs/LOG_ID",
249		Resource:  res,
250		Timestamp: ts,
251	}
252	got, err = fromLogEntry(&logEntry)
253	if err != nil {
254		t.Fatal(err)
255	}
256	if !ltesting.PayloadEqual(got.Payload, nil) {
257		t.Errorf("got %+v, want %+v", got.Payload, nil)
258	}
259}
260
261func TestListLogEntriesRequest(t *testing.T) {
262	for _, test := range []struct {
263		opts          []EntriesOption
264		resourceNames []string
265		filter        string
266		orderBy       string
267	}{
268		// Default is client's project ID, empty filter and orderBy.
269		{nil, []string{"projects/PROJECT_ID"}, "", ""},
270		{[]EntriesOption{NewestFirst(), Filter("f")},
271			[]string{"projects/PROJECT_ID"}, "f", "timestamp desc"},
272		{[]EntriesOption{ProjectIDs([]string{"foo"})},
273			[]string{"projects/foo"}, "", ""},
274		{[]EntriesOption{ResourceNames([]string{"folders/F", "organizations/O"})},
275			[]string{"folders/F", "organizations/O"}, "", ""},
276		{[]EntriesOption{NewestFirst(), Filter("f"), ProjectIDs([]string{"foo"})},
277			[]string{"projects/foo"}, "f", "timestamp desc"},
278		{[]EntriesOption{NewestFirst(), Filter("f"), ProjectIDs([]string{"foo"})},
279			[]string{"projects/foo"}, "f", "timestamp desc"},
280		// If there are repeats, last one wins.
281		{[]EntriesOption{NewestFirst(), Filter("no"), ProjectIDs([]string{"foo"}), Filter("f")},
282			[]string{"projects/foo"}, "f", "timestamp desc"},
283	} {
284		got := listLogEntriesRequest("projects/PROJECT_ID", test.opts)
285		want := &logpb.ListLogEntriesRequest{
286			ResourceNames: test.resourceNames,
287			Filter:        test.filter,
288			OrderBy:       test.orderBy,
289		}
290		if !proto.Equal(got, want) {
291			t.Errorf("%v:\ngot  %v\nwant %v", test.opts, got, want)
292		}
293	}
294}
295