github.com/google/osv-scalibr@v0.4.1/extractor/filesystem/os/winget/winget_test.go (about) 1 // Copyright 2025 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 package winget 16 17 import ( 18 "context" 19 "database/sql" 20 "fmt" 21 "io/fs" 22 "path/filepath" 23 "testing" 24 25 "github.com/google/go-cmp/cmp" 26 "github.com/google/osv-scalibr/extractor" 27 "github.com/google/osv-scalibr/extractor/filesystem/os/winget/metadata" 28 "github.com/google/osv-scalibr/extractor/filesystem/simplefileapi" 29 "github.com/google/osv-scalibr/purl" 30 "github.com/google/osv-scalibr/testing/fakefs" 31 _ "modernc.org/sqlite" 32 ) 33 34 func TestFileRequired(t *testing.T) { 35 tests := []struct { 36 name string 37 path string 38 want bool 39 }{ 40 { 41 name: "WingetInstalledDB_ReturnsTrue", 42 path: "/Users/test/AppData/Local/Packages/Microsoft.DesktopAppInstaller_8wekyb3d8bbwe/LocalState/Microsoft.Winget.Source_8wekyb3d8bbwe/installed.db", 43 want: true, 44 }, 45 { 46 name: "StoreEdgeFDInstalledDB_ReturnsTrue", 47 path: "/Users/test/AppData/Local/Packages/Microsoft.DesktopAppInstaller_8wekyb3d8bbwe/LocalState/StoreEdgeFD/installed.db", 48 want: true, 49 }, 50 { 51 name: "StateRepositoryMachine_ReturnsTrue", 52 path: "/ProgramData/Microsoft/Windows/AppRepository/StateRepository-Machine.srd", 53 want: true, 54 }, 55 { 56 name: "RandomSQLiteFile_ReturnsFalse", 57 path: "/some/random/path/database.db", 58 want: false, 59 }, 60 { 61 name: "WingetDBWrongPath_ReturnsFalse", 62 path: "/wrong/path/installed.db", 63 want: false, 64 }, 65 } 66 67 wingetExtractor := NewDefault() 68 for _, tt := range tests { 69 t.Run(tt.name, func(t *testing.T) { 70 api := simplefileapi.New(tt.path, fakefs.FakeFileInfo{ 71 FileName: filepath.Base(tt.path), 72 FileMode: fs.ModePerm, 73 FileSize: 1000, 74 }) 75 got := wingetExtractor.FileRequired(api) 76 if got != tt.want { 77 t.Errorf("FileRequired() = %v, want %v", got, tt.want) 78 } 79 }) 80 } 81 } 82 83 func TestExtract(t *testing.T) { 84 tests := []struct { 85 name string 86 setupDB func(string) error 87 want []*extractor.Package 88 wantErr bool 89 }{ 90 { 91 name: "ValidDatabase_ReturnsPackages", 92 setupDB: func(dbPath string) error { 93 return createTestDatabase(dbPath, []TestPackage{ 94 { 95 ID: "Git.Git", 96 Name: "Git", 97 Version: "2.50.1", 98 Moniker: "git", 99 Channel: "", 100 Tags: []string{"git", "vcs"}, 101 Commands: []string{"git"}, 102 }, 103 { 104 ID: "Microsoft.VisualStudioCode", 105 Name: "Microsoft Visual Studio Code", 106 Version: "1.103.1", 107 Moniker: "vscode", 108 Channel: "stable", 109 Tags: []string{"developer-tools", "editor"}, 110 Commands: []string{"code"}, 111 }, 112 }) 113 }, 114 want: []*extractor.Package{ 115 { 116 Name: "Git.Git", 117 Version: "2.50.1", 118 PURLType: purl.TypeWinget, 119 Locations: []string{"test.db"}, 120 Metadata: &metadata.Metadata{ 121 Name: "Git", 122 ID: "Git.Git", 123 Version: "2.50.1", 124 Moniker: "git", 125 Channel: "", 126 Tags: []string{"git", "vcs"}, 127 Commands: []string{"git"}, 128 }, 129 }, 130 { 131 Name: "Microsoft.VisualStudioCode", 132 Version: "1.103.1", 133 PURLType: purl.TypeWinget, 134 Locations: []string{"test.db"}, 135 Metadata: &metadata.Metadata{ 136 Name: "Microsoft Visual Studio Code", 137 ID: "Microsoft.VisualStudioCode", 138 Version: "1.103.1", 139 Moniker: "vscode", 140 Channel: "stable", 141 Tags: []string{"developer-tools", "editor"}, 142 Commands: []string{"code"}, 143 }, 144 }, 145 }, 146 wantErr: false, 147 }, 148 { 149 name: "EmptyDatabase_ReturnsEmpty", 150 setupDB: func(dbPath string) error { 151 return createTestDatabase(dbPath, []TestPackage{}) 152 }, 153 want: nil, 154 wantErr: false, 155 }, 156 { 157 name: "InvalidDatabase_ReturnsError", 158 setupDB: func(dbPath string) error { 159 db, err := sql.Open("sqlite", dbPath) 160 if err != nil { 161 return err 162 } 163 defer db.Close() 164 ctx := context.Background() 165 _, err = db.ExecContext(ctx, "CREATE TABLE test (id INTEGER)") 166 return err 167 }, 168 want: nil, 169 wantErr: true, 170 }, 171 } 172 173 for _, tt := range tests { 174 t.Run(tt.name, func(t *testing.T) { 175 tmpDir := t.TempDir() 176 dbPath := filepath.Join(tmpDir, "test.db") 177 178 if err := tt.setupDB(dbPath); err != nil { 179 t.Fatalf("Failed to setup test database: %v", err) 180 } 181 182 wingetExtractor := NewDefault() 183 184 // Create a custom Extract method that bypasses GetRealPath for testing 185 got, err := func() ([]*extractor.Package, error) { 186 db, err := sql.Open("sqlite", dbPath) 187 if err != nil { 188 return nil, fmt.Errorf("failed to open Winget database %s: %w", dbPath, err) 189 } 190 defer db.Close() 191 192 ctx := context.Background() 193 ext := wingetExtractor.(*Extractor) 194 if err := ext.validateDatabase(ctx, db); err != nil { 195 return nil, fmt.Errorf("invalid Winget database %s: %w", dbPath, err) 196 } 197 198 packages, err := ext.extractPackages(ctx, db) 199 if err != nil { 200 return nil, fmt.Errorf("failed to extract packages from %s: %w", dbPath, err) 201 } 202 203 var extPackages []*extractor.Package 204 for _, pkg := range packages { 205 if err := ctx.Err(); err != nil { 206 return nil, fmt.Errorf("%s halted due to context error: %w", wingetExtractor.Name(), err) 207 } 208 209 extPkg := &extractor.Package{ 210 Name: pkg.ID, 211 Version: pkg.Version, 212 PURLType: purl.TypeWinget, 213 Locations: []string{"test.db"}, 214 Metadata: &metadata.Metadata{ 215 Name: pkg.Name, 216 ID: pkg.ID, 217 Version: pkg.Version, 218 Moniker: pkg.Moniker, 219 Channel: pkg.Channel, 220 Tags: pkg.Tags, 221 Commands: pkg.Commands, 222 }, 223 } 224 extPackages = append(extPackages, extPkg) 225 } 226 227 return extPackages, nil 228 }() 229 if (err != nil) != tt.wantErr { 230 t.Errorf("Extract() error = %v, wantErr %v", err, tt.wantErr) 231 return 232 } 233 234 if !tt.wantErr { 235 if diff := cmp.Diff(tt.want, got); diff != "" { 236 t.Errorf("Extract() packages mismatch (-want +got):\n%s", diff) 237 } 238 } 239 }) 240 } 241 } 242 243 func TestExtractorInterface(t *testing.T) { 244 wingetExtractor := NewDefault() 245 246 if wingetExtractor.Name() != Name { 247 t.Errorf("Name() = %v, want %v", wingetExtractor.Name(), Name) 248 } 249 250 if wingetExtractor.Version() != 0 { 251 t.Errorf("Version() = %v, want %v", wingetExtractor.Version(), 0) 252 } 253 254 caps := wingetExtractor.Requirements() 255 if caps.OS != 2 { // OSWindows = 2 256 t.Errorf("Requirements().OS = %v, want 2 (OSWindows)", caps.OS) 257 } 258 259 if caps.RunningSystem { 260 t.Error("Requirements().RunningSystem should be false for filesystem extractor") 261 } 262 } 263 264 // TestPackage represents a test package for database creation 265 type TestPackage struct { 266 ID string 267 Name string 268 Version string 269 Moniker string 270 Channel string 271 Tags []string 272 Commands []string 273 } 274 275 // createTestDatabase creates a SQLite database with the Winget schema and test data 276 func createTestDatabase(dbPath string, packages []TestPackage) error { 277 db, err := sql.Open("sqlite", dbPath) 278 if err != nil { 279 return err 280 } 281 defer db.Close() 282 283 ctx := context.Background() 284 285 // Create schema 286 schema := ` 287 CREATE TABLE [metadata]( 288 [name] TEXT PRIMARY KEY NOT NULL, 289 [value] TEXT NOT NULL); 290 CREATE TABLE [ids](rowid INTEGER PRIMARY KEY, [id] TEXT NOT NULL); 291 CREATE UNIQUE INDEX [ids_pkindex] ON [ids]([id]); 292 CREATE TABLE [names](rowid INTEGER PRIMARY KEY, [name] TEXT NOT NULL); 293 CREATE UNIQUE INDEX [names_pkindex] ON [names]([name]); 294 CREATE TABLE [monikers](rowid INTEGER PRIMARY KEY, [moniker] TEXT NOT NULL); 295 CREATE UNIQUE INDEX [monikers_pkindex] ON [monikers]([moniker]); 296 CREATE TABLE [versions](rowid INTEGER PRIMARY KEY, [version] TEXT NOT NULL); 297 CREATE UNIQUE INDEX [versions_pkindex] ON [versions]([version]); 298 CREATE TABLE [channels](rowid INTEGER PRIMARY KEY, [channel] TEXT NOT NULL); 299 CREATE UNIQUE INDEX [channels_pkindex] ON [channels]([channel]); 300 CREATE TABLE [manifest](rowid INTEGER PRIMARY KEY, [id] INT64 NOT NULL, [name] INT64 NOT NULL, [moniker] INT64 NOT NULL, [version] INT64 NOT NULL, [channel] INT64 NOT NULL, [pathpart] INT64 NOT NULL, hash BLOB, arp_min_version INT64, arp_max_version INT64); 301 CREATE TABLE [tags](rowid INTEGER PRIMARY KEY, [tag] TEXT NOT NULL); 302 CREATE UNIQUE INDEX [tags_pkindex] ON [tags]([tag]); 303 CREATE TABLE [tags_map]([manifest] INT64 NOT NULL, [tag] INT64 NOT NULL, PRIMARY KEY([tag], [manifest])) WITHOUT ROWID; 304 CREATE TABLE [commands](rowid INTEGER PRIMARY KEY, [command] TEXT NOT NULL); 305 CREATE UNIQUE INDEX [commands_pkindex] ON [commands]([command]); 306 CREATE TABLE [commands_map]([manifest] INT64 NOT NULL, [command] INT64 NOT NULL, PRIMARY KEY([command], [manifest])) WITHOUT ROWID; 307 ` 308 309 _, err = db.ExecContext(ctx, schema) 310 if err != nil { 311 return err 312 } 313 314 // Insert test data 315 for i, pkg := range packages { 316 manifestID := i + 1 317 318 // Insert lookup table values 319 _, err = db.ExecContext(ctx, "INSERT INTO ids (rowid, id) VALUES (?, ?)", manifestID, pkg.ID) 320 if err != nil { 321 return err 322 } 323 324 _, err = db.ExecContext(ctx, "INSERT INTO names (rowid, name) VALUES (?, ?)", manifestID, pkg.Name) 325 if err != nil { 326 return err 327 } 328 329 _, err = db.ExecContext(ctx, "INSERT INTO monikers (rowid, moniker) VALUES (?, ?)", manifestID, pkg.Moniker) 330 if err != nil { 331 return err 332 } 333 334 _, err = db.ExecContext(ctx, "INSERT INTO versions (rowid, version) VALUES (?, ?)", manifestID, pkg.Version) 335 if err != nil { 336 return err 337 } 338 339 _, err = db.ExecContext(ctx, "INSERT INTO channels (rowid, channel) VALUES (?, ?)", manifestID, pkg.Channel) 340 if err != nil { 341 return err 342 } 343 344 // Insert manifest 345 _, err = db.ExecContext(ctx, "INSERT INTO manifest (rowid, id, name, moniker, version, channel, pathpart) VALUES (?, ?, ?, ?, ?, ?, ?)", 346 manifestID, manifestID, manifestID, manifestID, manifestID, manifestID, -1) 347 if err != nil { 348 return err 349 } 350 351 // Insert tags 352 for j, tag := range pkg.Tags { 353 tagID := i*100 + j + 1 354 _, err = db.ExecContext(ctx, "INSERT INTO tags (rowid, tag) VALUES (?, ?)", tagID, tag) 355 if err != nil { 356 return err 357 } 358 _, err = db.ExecContext(ctx, "INSERT INTO tags_map (manifest, tag) VALUES (?, ?)", manifestID, tagID) 359 if err != nil { 360 return err 361 } 362 } 363 364 // Insert commands 365 for j, command := range pkg.Commands { 366 commandID := i*100 + j + 1 367 _, err = db.ExecContext(ctx, "INSERT INTO commands (rowid, command) VALUES (?, ?)", commandID, command) 368 if err != nil { 369 return err 370 } 371 _, err = db.ExecContext(ctx, "INSERT INTO commands_map (manifest, command) VALUES (?, ?)", manifestID, commandID) 372 if err != nil { 373 return err 374 } 375 } 376 } 377 378 return nil 379 }