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		integrationTest = true
84		ts := testutil.TokenSource(ctx, logging.AdminScope)
85		if ts == nil {
86			log.Fatal("The project key must be set. See CONTRIBUTING.md for details")
87		}
88		log.Printf("running integration tests with project %s", testProjectID)
89		newClient = func(ctx context.Context, projectID string) *Client {
90			c, err := NewClient(ctx, projectID, option.WithTokenSource(ts),
91				option.WithGRPCDialOption(grpc.WithBlock()))
92			if err != nil {
93				log.Fatalf("creating prod client: %v", err)
94			}
95			return c
96		}
97	}
98	client = newClient(ctx, testProjectID)
99	initMetrics(ctx)
100	cleanup := initSinks(ctx)
101	exit := m.Run()
102	cleanup()
103	client.Close()
104	os.Exit(exit)
105}
106
107// EntryIterator and DeleteLog are tested in the logging package.
108
109func TestClientClose(t *testing.T) {
110	c := newClient(context.Background(), testProjectID)
111	if err := c.Close(); err != nil {
112		t.Errorf("want got %v, want nil", err)
113	}
114}
115
116func TestFromLogEntry(t *testing.T) {
117	now := time.Now()
118	res := &mrpb.MonitoredResource{Type: "global"}
119	ts, err := ptypes.TimestampProto(now)
120	if err != nil {
121		t.Fatal(err)
122	}
123	logEntry := logpb.LogEntry{
124		LogName:   "projects/PROJECT_ID/logs/LOG_ID",
125		Resource:  res,
126		Payload:   &logpb.LogEntry_TextPayload{TextPayload: "hello"},
127		Timestamp: ts,
128		Severity:  logtypepb.LogSeverity_INFO,
129		InsertId:  "123",
130		HttpRequest: &logtypepb.HttpRequest{
131			RequestMethod:                  "GET",
132			RequestUrl:                     "http:://example.com/path?q=1",
133			RequestSize:                    100,
134			Status:                         200,
135			ResponseSize:                   25,
136			Latency:                        &durpb.Duration{Seconds: 100},
137			UserAgent:                      "user-agent",
138			RemoteIp:                       "127.0.0.1",
139			Referer:                        "referer",
140			CacheHit:                       true,
141			CacheValidatedWithOriginServer: true,
142		},
143		Labels: map[string]string{
144			"a": "1",
145			"b": "two",
146			"c": "true",
147		},
148		SourceLocation: &logpb.LogEntrySourceLocation{
149			File:     "some_file.go",
150			Line:     1,
151			Function: "someFunction",
152		},
153	}
154	u, err := url.Parse("http:://example.com/path?q=1")
155	if err != nil {
156		t.Fatal(err)
157	}
158	want := &logging.Entry{
159		LogName:   "projects/PROJECT_ID/logs/LOG_ID",
160		Resource:  res,
161		Timestamp: now.In(time.UTC),
162		Severity:  logging.Info,
163		Payload:   "hello",
164		Labels: map[string]string{
165			"a": "1",
166			"b": "two",
167			"c": "true",
168		},
169		InsertID: "123",
170		HTTPRequest: &logging.HTTPRequest{
171			Request: &http.Request{
172				Method: "GET",
173				URL:    u,
174				Header: map[string][]string{
175					"User-Agent": {"user-agent"},
176					"Referer":    {"referer"},
177				},
178			},
179			RequestSize:                    100,
180			Status:                         200,
181			ResponseSize:                   25,
182			Latency:                        100 * time.Second,
183			RemoteIP:                       "127.0.0.1",
184			CacheHit:                       true,
185			CacheValidatedWithOriginServer: true,
186		},
187		SourceLocation: &logpb.LogEntrySourceLocation{
188			File:     "some_file.go",
189			Line:     1,
190			Function: "someFunction",
191		},
192	}
193	got, err := fromLogEntry(&logEntry)
194	if err != nil {
195		t.Fatal(err)
196	}
197	if diff := testutil.Diff(got, want, cmpopts.IgnoreUnexported(http.Request{})); diff != "" {
198		t.Errorf("FullEntry:\n%s", diff)
199	}
200
201	// Proto payload.
202	alog := &audit.AuditLog{
203		ServiceName:  "svc",
204		MethodName:   "method",
205		ResourceName: "shelves/S/books/B",
206	}
207	any, err := ptypes.MarshalAny(alog)
208	if err != nil {
209		t.Fatal(err)
210	}
211	logEntry = logpb.LogEntry{
212		LogName:   "projects/PROJECT_ID/logs/LOG_ID",
213		Resource:  res,
214		Timestamp: ts,
215		Payload:   &logpb.LogEntry_ProtoPayload{ProtoPayload: any},
216	}
217	got, err = fromLogEntry(&logEntry)
218	if err != nil {
219		t.Fatal(err)
220	}
221	if !ltesting.PayloadEqual(got.Payload, alog) {
222		t.Errorf("got %+v, want %+v", got.Payload, alog)
223	}
224
225	// JSON payload.
226	jstruct := &structpb.Struct{Fields: map[string]*structpb.Value{
227		"f": {Kind: &structpb.Value_NumberValue{NumberValue: 3.1}},
228	}}
229	logEntry = logpb.LogEntry{
230		LogName:   "projects/PROJECT_ID/logs/LOG_ID",
231		Resource:  res,
232		Timestamp: ts,
233		Payload:   &logpb.LogEntry_JsonPayload{JsonPayload: jstruct},
234	}
235	got, err = fromLogEntry(&logEntry)
236	if err != nil {
237		t.Fatal(err)
238	}
239	if !ltesting.PayloadEqual(got.Payload, jstruct) {
240		t.Errorf("got %+v, want %+v", got.Payload, jstruct)
241	}
242}
243
244func TestListLogEntriesRequest(t *testing.T) {
245	for _, test := range []struct {
246		opts          []EntriesOption
247		resourceNames []string
248		filter        string
249		orderBy       string
250	}{
251		// Default is client's project ID, empty filter and orderBy.
252		{nil, []string{"projects/PROJECT_ID"}, "", ""},
253		{[]EntriesOption{NewestFirst(), Filter("f")},
254			[]string{"projects/PROJECT_ID"}, "f", "timestamp desc"},
255		{[]EntriesOption{ProjectIDs([]string{"foo"})},
256			[]string{"projects/foo"}, "", ""},
257		{[]EntriesOption{ResourceNames([]string{"folders/F", "organizations/O"})},
258			[]string{"folders/F", "organizations/O"}, "", ""},
259		{[]EntriesOption{NewestFirst(), Filter("f"), ProjectIDs([]string{"foo"})},
260			[]string{"projects/foo"}, "f", "timestamp desc"},
261		{[]EntriesOption{NewestFirst(), Filter("f"), ProjectIDs([]string{"foo"})},
262			[]string{"projects/foo"}, "f", "timestamp desc"},
263		// If there are repeats, last one wins.
264		{[]EntriesOption{NewestFirst(), Filter("no"), ProjectIDs([]string{"foo"}), Filter("f")},
265			[]string{"projects/foo"}, "f", "timestamp desc"},
266	} {
267		got := listLogEntriesRequest("projects/PROJECT_ID", test.opts)
268		want := &logpb.ListLogEntriesRequest{
269			ResourceNames: test.resourceNames,
270			Filter:        test.filter,
271			OrderBy:       test.orderBy,
272		}
273		if !proto.Equal(got, want) {
274			t.Errorf("%v:\ngot  %v\nwant %v", test.opts, got, want)
275		}
276	}
277}
278