| // Copyright 2026 Google LLC |
| // |
| // Licensed under the Apache License, Version 2.0 (the "License"); |
| // you may not use this file except in compliance with the License. |
| // You may obtain a copy of the License at |
| // |
| // http://www.apache.org/licenses/LICENSE-2.0 |
| // |
| // Unless required by applicable law or agreed to in writing, software |
| // distributed under the License is distributed on an "AS IS" BASIS, |
| // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| // See the License for the specific language governing permissions and |
| // limitations under the License. |
| |
| package rsmsg |
| |
| import ( |
| "strings" |
| "testing" |
| |
| "github.com/google/go-cmp/cmp" |
| "google.golang.org/protobuf/testing/protocmp" |
| |
| rspb "google.golang.org/genproto/googleapis/devtools/resultstore/v2" |
| |
| _ "github.com/bazelbuild/rsclient/internal/pkg/testsetup" |
| ) |
| |
| func TestEncodeID(t *testing.T) { |
| tests := []struct { |
| input string |
| escapeChar string |
| want string |
| }{ |
| {"abc-123_xyz", "%", "abc-123_xyz"}, |
| {"id with spaces", "%", "id%20with%20spaces"}, |
| {"id/with/slashes", "%", "id%2Fwith%2Fslashes"}, |
| {"id.with.dots", "%", "id%2Ewith%2Edots"}, |
| {"abc-123_xyz", "_", "abc-123__xyz"}, // _ is escaped as __ |
| {"id with spaces", "_", "id_20with_20spaces"}, |
| } |
| |
| for _, tt := range tests { |
| got := EncodeID(tt.input, tt.escapeChar) |
| if got != tt.want { |
| t.Errorf("EncodeID(%q, %q) = %q, want %q", tt.input, tt.escapeChar, got, tt.want) |
| } |
| } |
| } |
| |
| func TestMangleInvocation(t *testing.T) { |
| inv := &rspb.Invocation{ |
| Id: &rspb.Invocation_Id{InvocationId: "id with spaces"}, |
| Name: "projects/p/instances/i/invocations/id with spaces", |
| } |
| |
| MangleInvocation(inv) |
| |
| wantID := "id%20with%20spaces" |
| wantName := "projects/p/instances/i/invocations/id%20with%20spaces" |
| |
| if inv.Id.InvocationId != wantID { |
| t.Errorf("InvocationId = %q, want %q", inv.Id.InvocationId, wantID) |
| } |
| if inv.Name != wantName { |
| t.Errorf("Name = %q, want %q", inv.Name, wantName) |
| } |
| } |
| |
| func TestMangleTarget(t *testing.T) { |
| target := &rspb.Target{ |
| Id: &rspb.Target_Id{ |
| InvocationId: "inv id", |
| TargetId: "tgt id", |
| }, |
| Name: "projects/p/instances/i/invocations/inv id/targets/tgt id", |
| } |
| |
| MangleTarget(target) |
| |
| wantInvID := "inv%20id" |
| wantTgtID := "tgt%20id" |
| wantName := "projects/p/instances/i/invocations/inv%20id/targets/tgt%20id" |
| |
| if target.Id.InvocationId != wantInvID { |
| t.Errorf("InvocationId = %q, want %q", target.Id.InvocationId, wantInvID) |
| } |
| if target.Id.TargetId != wantTgtID { |
| t.Errorf("TargetId = %q, want %q", target.Id.TargetId, wantTgtID) |
| } |
| if target.Name != wantName { |
| t.Errorf("Name = %q, want %q", target.Name, wantName) |
| } |
| } |
| |
| func TestMangleConfiguration(t *testing.T) { |
| cfg := &rspb.Configuration{ |
| Id: &rspb.Configuration_Id{InvocationId: "inv id", ConfigurationId: "cfg id"}, |
| Name: "projects/p/invocations/inv id/configs/cfg id", |
| } |
| |
| MangleConfiguration(cfg) |
| |
| wantInvID := "inv%20id" |
| wantCfgID := "cfg%20id" |
| wantName := "projects/p/invocations/inv%20id/configs/cfg%20id" |
| |
| if cfg.Id.InvocationId != wantInvID { |
| t.Errorf("InvocationId = %q, want %q", cfg.Id.InvocationId, wantInvID) |
| } |
| if cfg.Id.ConfigurationId != wantCfgID { |
| t.Errorf("ConfigurationId = %q, want %q", cfg.Id.ConfigurationId, wantCfgID) |
| } |
| if cfg.Name != wantName { |
| t.Errorf("Name = %q, want %q", cfg.Name, wantName) |
| } |
| } |
| |
| func TestMangleConfiguredTarget(t *testing.T) { |
| ct := &rspb.ConfiguredTarget{ |
| Id: &rspb.ConfiguredTarget_Id{ |
| InvocationId: "inv id", |
| TargetId: "tgt id", |
| ConfigurationId: "cfg id", |
| }, |
| Name: "projects/p/invocations/inv id/targets/tgt id/configuredTargets/cfg id", |
| } |
| |
| MangleConfiguredTarget(ct) |
| |
| wantInvID := "inv%20id" |
| wantTgtID := "tgt%20id" |
| wantCfgID := "cfg%20id" |
| wantName := "projects/p/invocations/inv%20id/targets/tgt%20id/configuredTargets/cfg%20id" |
| |
| if ct.Id.InvocationId != wantInvID { |
| t.Errorf("InvocationId = %q, want %q", ct.Id.InvocationId, wantInvID) |
| } |
| if ct.Id.TargetId != wantTgtID { |
| t.Errorf("TargetId = %q, want %q", ct.Id.TargetId, wantTgtID) |
| } |
| if ct.Id.ConfigurationId != wantCfgID { |
| t.Errorf("ConfigurationId = %q, want %q", ct.Id.ConfigurationId, wantCfgID) |
| } |
| if ct.Name != wantName { |
| t.Errorf("Name = %q, want %q", ct.Name, wantName) |
| } |
| } |
| |
| func TestMangleAction(t *testing.T) { |
| action := &rspb.Action{ |
| Id: &rspb.Action_Id{ |
| InvocationId: "inv id", |
| TargetId: "tgt id", |
| ConfigurationId: "cfg id", |
| ActionId: "act id", |
| }, |
| Name: "projects/p/instances/i/invocations/inv id/targets/tgt id/configuredTargets/cfg id/actions/act id", |
| } |
| |
| MangleAction(action) |
| |
| wantInvID := "inv%20id" |
| wantTgtID := "tgt%20id" |
| wantCfgID := "cfg%20id" |
| wantActID := "act_20id" // Actions use _ encoding |
| wantName := "projects/p/instances/i/invocations/inv%20id/targets/tgt%20id/configuredTargets/cfg%20id/actions/act_20id" |
| |
| if action.Id.InvocationId != wantInvID { |
| t.Errorf("InvocationId = %q, want %q", action.Id.InvocationId, wantInvID) |
| } |
| if action.Id.TargetId != wantTgtID { |
| t.Errorf("TargetId = %q, want %q", action.Id.TargetId, wantTgtID) |
| } |
| if action.Id.ConfigurationId != wantCfgID { |
| t.Errorf("ConfigurationId = %q, want %q", action.Id.ConfigurationId, wantCfgID) |
| } |
| if action.Id.ActionId != wantActID { |
| t.Errorf("ActionId = %q, want %q", action.Id.ActionId, wantActID) |
| } |
| if action.Name != wantName { |
| t.Errorf("Name = %q, want %q", action.Name, wantName) |
| } |
| } |
| |
| func TestMangleResourceIDs(t *testing.T) { |
| req := &rspb.UploadRequest{ |
| Resource: &rspb.UploadRequest_Invocation{ |
| Invocation: &rspb.Invocation{ |
| Id: &rspb.Invocation_Id{InvocationId: "inv id"}, |
| Name: "invocations/inv id", |
| }, |
| }, |
| } |
| |
| MangleResourceIDs(req) |
| |
| got := req.GetInvocation().Id.InvocationId |
| want := "inv%20id" |
| if got != want { |
| t.Errorf("InvocationId = %q, want %q", got, want) |
| } |
| } |
| |
| func TestIDTransformer_CustomStrategy(t *testing.T) { |
| // A strategy that just uppercases everything. |
| upper := &IDTransformer{ |
| ActionID: strings.ToUpper, |
| OtherID: strings.ToUpper, |
| } |
| |
| inv := &rspb.Invocation{ |
| Id: &rspb.Invocation_Id{InvocationId: "inv-abc"}, |
| Name: "projects/p/invocations/inv-abc", |
| } |
| |
| upper.Invocation(inv) |
| |
| wantID := "INV-ABC" |
| wantName := "projects/p/invocations/INV-ABC" |
| |
| if inv.Id.InvocationId != wantID { |
| t.Errorf("InvocationId = %q, want %q", inv.Id.InvocationId, wantID) |
| } |
| if inv.Name != wantName { |
| t.Errorf("Name = %q, want %q", inv.Name, wantName) |
| } |
| } |
| |
| func TestMangleResourceIDs_Complex(t *testing.T) { |
| // Ensure that all top-level IDs are mangled correctly. |
| req := &rspb.UploadRequest{ |
| Id: &rspb.UploadRequest_Id{ |
| TargetId: "tgt id", |
| ConfigurationId: "cfg id", |
| ActionId: "act id", |
| }, |
| } |
| |
| MangleResourceIDs(req) |
| |
| want := &rspb.UploadRequest_Id{ |
| TargetId: "tgt%20id", |
| ConfigurationId: "cfg%20id", |
| ActionId: "act_20id", |
| } |
| |
| if diff := cmp.Diff(want, req.Id, protocmp.Transform()); diff != "" { |
| t.Errorf("UploadRequest.Id mismatch (-want +got):\n%s", diff) |
| } |
| } |
| |
| func TestDedupeStrings(t *testing.T) { |
| tests := []struct { |
| name string |
| input []string |
| want []string |
| }{ |
| { |
| name: "no duplicates", |
| input: []string{"a", "b", "c"}, |
| want: []string{"a", "b", "c"}, |
| }, |
| { |
| name: "duplicates removed", |
| input: []string{"a", "b", "a", "c", "b"}, |
| want: []string{"a", "b", "c"}, |
| }, |
| { |
| name: "empty slice", |
| input: []string{}, |
| want: []string{}, |
| }, |
| { |
| name: "nil slice", |
| input: nil, |
| want: []string{}, |
| }, |
| } |
| |
| for _, tt := range tests { |
| t.Run(tt.name, func(t *testing.T) { |
| got := DedupeStrings(tt.input) |
| if diff := cmp.Diff(tt.want, got); diff != "" { |
| t.Errorf("DedupeStrings() mismatch (-want +got):\n%s", diff) |
| } |
| }) |
| } |
| } |
| |
| func TestDedupeInvocationAttributes(t *testing.T) { |
| attr := &rspb.InvocationAttributes{ |
| Labels: []string{"a", "b", "a"}, |
| Users: []string{"user1", "user2", "user1"}, |
| } |
| |
| DedupeInvocationAttributes(attr) |
| |
| want := &rspb.InvocationAttributes{ |
| Labels: []string{"a", "b"}, |
| Users: []string{"user1", "user2"}, |
| } |
| |
| if diff := cmp.Diff(want, attr, protocmp.Transform()); diff != "" { |
| t.Errorf("DedupeInvocationAttributes() mismatch (-want +got):\n%s", diff) |
| } |
| } |
| |
| func TestComposeInvocation(t *testing.T) { |
| tool := &rspb.Invocation{ |
| Id: &rspb.Invocation_Id{InvocationId: "tool-id"}, |
| InvocationAttributes: &rspb.InvocationAttributes{ |
| Labels: []string{"tool-label"}, |
| }, |
| } |
| partial := &rspb.Invocation{ |
| InvocationAttributes: &rspb.InvocationAttributes{ |
| Labels: []string{"proxy-label"}, |
| Users: []string{"proxy-user"}, |
| }, |
| } |
| projectID := "test-project" |
| |
| got := ComposeInvocation(tool, partial, projectID) |
| |
| want := &rspb.Invocation{ |
| Id: &rspb.Invocation_Id{InvocationId: "tool-id"}, |
| InvocationAttributes: &rspb.InvocationAttributes{ |
| ProjectId: "test-project", |
| Labels: []string{"tool-label", "proxy-label"}, |
| Users: []string{"proxy-user"}, |
| }, |
| StatusAttributes: &rspb.StatusAttributes{ |
| Status: rspb.Status_BUILDING, |
| }, |
| } |
| |
| if diff := cmp.Diff(want, got, protocmp.Transform()); diff != "" { |
| t.Errorf("ComposeInvocation() mismatch (-want +got):\n%s", diff) |
| } |
| } |