package main import ( "bytes" "context" "database/sql" "errors" "io" "net/url" "os" "path/filepath" "runtime" "strings" "testing" "cmbuyer/admin/internal/deviceauth" "cmbuyer/admin/internal/migrations" "cmbuyer/admin/internal/storage/sqlite" ) func TestIssueListAndIdempotentRevokeNeverRediscloseSecret(t *testing.T) { databaseSource := migratedDatabase(t) var issued bytes.Buffer if err := run(context.Background(), []string{"-database", databaseSource, "issue", "-name", "采购工具一号"}, &issued, io.Discard); err != nil { t.Fatalf("issue: %v", err) } fields := outputFields(t, issued.String()) deviceID, token := fields["device_id"], fields["token"] if len(token) != 64 || strings.Count(issued.String(), token) != 1 { t.Fatalf("issue token occurrence/length = %d/%d", strings.Count(issued.String(), token), len(token)) } var listed bytes.Buffer if err := run(context.Background(), []string{"-database", databaseSource, "list"}, &listed, io.Discard); err != nil { t.Fatalf("list: %v", err) } assertNoSecretMetadata(t, listed.String(), token) if !strings.Contains(listed.String(), deviceID) || !strings.Contains(listed.String(), "采购工具一号") { t.Fatalf("list omitted safe metadata: %s", listed.String()) } var revoked bytes.Buffer if err := run(context.Background(), []string{"-database", databaseSource, "revoke", "-device-id", deviceID}, &revoked, io.Discard); err != nil { t.Fatalf("revoke: %v", err) } assertNoSecretMetadata(t, revoked.String(), token) if !strings.Contains(revoked.String(), `"revoked_now":true`) { t.Fatalf("first revoke output = %s", revoked.String()) } var repeated bytes.Buffer if err := run(context.Background(), []string{"-database", databaseSource, "revoke", "-device-id", deviceID}, &repeated, io.Discard); err != nil { t.Fatalf("repeat revoke: %v", err) } assertNoSecretMetadata(t, repeated.String(), token) if !strings.Contains(repeated.String(), `"revoked_now":false`) { t.Fatalf("repeat revoke output = %s", repeated.String()) } } func TestIssueOutputFailureLeavesCommittedCredentialWithoutSecretInError(t *testing.T) { databaseSource := migratedDatabase(t) writer := &recordingFailureWriter{} err := run(context.Background(), []string{"-database", databaseSource, "issue", "-name", "output failure"}, writer, io.Discard) if err == nil || err.Error() != "write issued device credential" { t.Fatalf("issue output failure error = %v", err) } fields := outputFields(t, writer.contents.String()) if strings.Contains(err.Error(), fields["token"]) { t.Fatal("output error disclosed token") } database, err := sql.Open("sqlite3", databaseSource) if err != nil { t.Fatalf("open database: %v", err) } defer database.Close() var count int if err := database.QueryRow(`SELECT COUNT(*) FROM device_credentials`).Scan(&count); err != nil || count != 1 { t.Fatalf("committed credential count = %d, err=%v", count, err) } } func TestCLIRequiresPreMigratedExplicitDatabase(t *testing.T) { if err := run(context.Background(), []string{"list"}, io.Discard, io.Discard); err == nil { t.Fatal("command without -database succeeded") } missing := filepath.Join(t.TempDir(), "missing.db") if err := run(context.Background(), []string{"-database", missing, "list"}, io.Discard, io.Discard); err == nil { t.Fatal("list opened a missing database") } if _, err := os.Stat(missing); !errors.Is(err, os.ErrNotExist) { t.Fatalf("missing database was created: %v", err) } unmigrated := filepath.Join(t.TempDir(), "unmigrated.db") unmigratedDatabase, err := sqlite.Open(unmigrated) if err != nil { t.Fatalf("create unmigrated database: %v", err) } if _, err := unmigratedDatabase.Exec(`CREATE TABLE unrelated (id INTEGER)`); err != nil { _ = unmigratedDatabase.Close() t.Fatalf("initialize unmigrated database: %v", err) } if err := unmigratedDatabase.Close(); err != nil { t.Fatalf("close unmigrated database: %v", err) } if err := run(context.Background(), []string{"-database", unmigrated, "list"}, io.Discard, io.Discard); err == nil { t.Fatal("list accepted an unmigrated database") } database, err := sql.Open("sqlite3", unmigrated) if err != nil { t.Fatalf("open unmigrated database: %v", err) } defer database.Close() var count int if err := database.QueryRow(`SELECT COUNT(*) FROM sqlite_master WHERE type='table' AND name='device_credentials'`).Scan(&count); err != nil || count != 0 { t.Fatalf("device_credentials table count = %d, err=%v", count, err) } undeclared := filepath.Join(t.TempDir(), "undeclared.db") if err := run(context.Background(), []string{"-database", undeclared, "rotate"}, io.Discard, io.Discard); err == nil { t.Fatal("undeclared command succeeded") } if _, err := os.Stat(undeclared); !errors.Is(err, os.ErrNotExist) { t.Fatalf("undeclared command opened database: %v", err) } invalidIssue := filepath.Join(t.TempDir(), "invalid-issue.db") if err := run(context.Background(), []string{"-database", invalidIssue, "issue", "-name", " padded"}, io.Discard, io.Discard); !errors.Is(err, deviceauth.ErrInvalidCredential) { t.Fatalf("invalid issue error = %v", err) } if _, err := os.Stat(invalidIssue); !errors.Is(err, os.ErrNotExist) { t.Fatalf("invalid issue opened database: %v", err) } invalidRevoke := filepath.Join(t.TempDir(), "invalid-revoke.db") if err := run(context.Background(), []string{"-database", invalidRevoke, "revoke", "-device-id", "not-a-uuid"}, io.Discard, io.Discard); !errors.Is(err, deviceauth.ErrInvalidCredential) { t.Fatalf("invalid revoke error = %v", err) } if _, err := os.Stat(invalidRevoke); !errors.Is(err, os.ErrNotExist) { t.Fatalf("invalid revoke opened database: %v", err) } migrated := migratedDatabase(t) unknownID := "13c9f507-7473-4fa6-8d71-8786c34c6301" var unknownOutput bytes.Buffer err = run(context.Background(), []string{"-database", migrated, "revoke", "-device-id", unknownID}, &unknownOutput, io.Discard) if !errors.Is(err, deviceauth.ErrCredentialNotFound) || unknownOutput.Len() != 0 || strings.Contains(err.Error(), unknownID) { t.Fatalf("unknown revoke = output %q, error %v", unknownOutput.String(), err) } } func TestExistingSQLiteDataSourcePreservesSafeOptionsAndRejectsCreationModes(t *testing.T) { databaseSource := migratedDatabase(t) fileURI := (&url.URL{ Scheme: "file", Path: sqliteURIPath(databaseSource), RawQuery: "_busy_timeout=5000&cache=shared", }).String() normalized, err := existingSQLiteDataSource(fileURI) if err != nil { t.Fatalf("normalize file URI: %v", err) } parsed, err := url.Parse(normalized) if err != nil { t.Fatalf("parse normalized URI: %v", err) } query := parsed.Query() if query.Get("mode") != "rw" || query.Get("_busy_timeout") != "5000" || query.Get("cache") != "shared" { t.Fatalf("normalized query = %v", query) } if err := run(context.Background(), []string{"-database", fileURI, "list"}, io.Discard, io.Discard); err != nil { t.Fatalf("list existing file URI: %v", err) } plainNormalized, err := existingSQLiteDataSource(databaseSource + "?_foreign_keys=on") if err != nil { t.Fatalf("normalize ordinary path: %v", err) } plainURI, err := url.Parse(plainNormalized) if err != nil || plainURI.Scheme != "file" || plainURI.Query().Get("mode") != "rw" || plainURI.Query().Get("_foreign_keys") != "on" { t.Fatalf("ordinary path normalization = %q, err=%v", plainNormalized, err) } missing := filepath.Join(t.TempDir(), "missing-uri.db") missingURI := (&url.URL{Scheme: "file", Path: sqliteURIPath(missing)}).String() if err := run(context.Background(), []string{"-database", missingURI, "list"}, io.Discard, io.Discard); err == nil { t.Fatal("missing file URI succeeded") } if _, err := os.Stat(missing); !errors.Is(err, os.ErrNotExist) { t.Fatalf("missing file URI created a file: %v", err) } for _, scheme := range []string{"FILE", "File"} { mixedMissing := filepath.Join(t.TempDir(), strings.ToLower(scheme)+"-missing.db") canonical := (&url.URL{Scheme: "file", Path: sqliteURIPath(mixedMissing)}).String() mixedURI := scheme + canonical[len("file"):] normalized, err := existingSQLiteDataSource(mixedURI) if err != nil || !strings.HasPrefix(normalized, "file:") { t.Fatalf("normalize %s URI = %q, err=%v", scheme, normalized, err) } if err := run(context.Background(), []string{"-database", mixedURI, "list"}, io.Discard, io.Discard); err == nil { t.Fatalf("missing %s URI succeeded", scheme) } if _, err := os.Stat(mixedMissing); !errors.Is(err, os.ErrNotExist) { t.Fatalf("missing %s URI created a file: %v", scheme, err) } } for name, source := range map[string]string{ "plain memory": ":memory:", "URI memory": "file::memory:?cache=shared", "memory mode": fileURI + "&mode=memory", "read only mode": fileURI + "&mode=ro", "create mode": fileURI + "&mode=rwc", "duplicate mode": fileURI + "&mode=rw&mode=rw", "immutable": fileURI + "&immutable=1", "query only": fileURI + "&_query_only=1", "remote authority": "file://server/share/database.db?mode=rw", } { t.Run(name, func(t *testing.T) { if _, err := existingSQLiteDataSource(source); err == nil { t.Fatalf("unsafe source accepted: %q", source) } }) } } func sqliteURIPath(path string) string { normalized := filepath.ToSlash(path) if filepath.VolumeName(path) != "" && !strings.HasPrefix(normalized, "/") { return "/" + normalized } return normalized } type recordingFailureWriter struct { contents bytes.Buffer } func (writer *recordingFailureWriter) Write(value []byte) (int, error) { _, _ = writer.contents.Write(value) return 0, errors.New("injected stdout failure") } func assertNoSecretMetadata(t *testing.T, output, token string) { t.Helper() if strings.Contains(output, token) || strings.Contains(output, "token") || strings.Contains(output, "hash") || strings.Contains(output, "sha256") { t.Fatalf("metadata output disclosed secret material: %s", output) } } func outputFields(t *testing.T, output string) map[string]string { t.Helper() fields := make(map[string]string) for _, line := range strings.Split(strings.TrimSpace(output), "\n") { name, value, found := strings.Cut(line, "=") if !found || name == "" || value == "" { t.Fatalf("invalid issue output line %q", line) } fields[name] = value } for _, required := range []string{"device_id", "display_name", "token", "created_at"} { if fields[required] == "" { t.Fatalf("issue output missing %s: %q", required, output) } } return fields } func migratedDatabase(t *testing.T) string { t.Helper() databaseSource := filepath.Join(t.TempDir(), "credentials.db") database, err := sqlite.Open(databaseSource) if err != nil { t.Fatalf("open database: %v", err) } if err := migrations.Up(context.Background(), database, commandMigrationDirectory(t)); err != nil { _ = database.Close() t.Fatalf("migrate database: %v", err) } if err := database.Close(); err != nil { t.Fatalf("close migrated database: %v", err) } return databaseSource } func commandMigrationDirectory(t *testing.T) string { t.Helper() _, file, _, ok := runtime.Caller(0) if !ok { t.Fatal("locate migrations") } return filepath.Join(filepath.Dir(file), "..", "..", "migrations") }