diff --git a/b2/accountinfo_edges_test.go b/b2/accountinfo_edges_test.go new file mode 100644 index 0000000..80bf830 --- /dev/null +++ b/b2/accountinfo_edges_test.go @@ -0,0 +1,88 @@ +package b2 + +import ( + "bytes" + "encoding/json" + "errors" + "os" + "path/filepath" + "strings" + "testing" +) + +func TestAccountInfoWriteSizeBoundary(t *testing.T) { + for _, test := range []struct { + name string + size int + want bool + }{ + {"cap minus one", 1_048_575, true}, + {"cap", 1_048_576, true}, + {"cap plus one", 1_048_577, false}, + } { + t.Run(test.name, func(t *testing.T) { + data := sampleAuthData() + data.AuthToken = "" + empty, err := json.Marshal(data) + if err != nil { + t.Fatal(err) + } + data.AuthToken = strings.Repeat("x", test.size-len(empty)-1) + path := filepath.Join(t.TempDir(), "private", "session.json") + store := NewAccountInfoFile(path) + store.Set(data) + if !test.want { + if err := store.Err(); err == nil || !strings.Contains(err.Error(), "exceeds") { + t.Fatalf("Set %d-byte session error = %v, want size rejection", test.size, err) + } + if _, err := os.Stat(path); !os.IsNotExist(err) { + t.Fatalf("rejected %d-byte session created a file: %v", test.size, err) + } + return + } + if err := store.Err(); err != nil { + t.Fatalf("Set %d-byte session: %v", test.size, err) + } + content, err := os.ReadFile(path) + if err != nil { + t.Fatal(err) + } + if len(content) != test.size { + t.Fatalf("session file bytes = %d, want %d", len(content), test.size) + } + }) + } +} + +func TestAccountInfoReadSizeBoundary(t *testing.T) { + for _, test := range []struct { + name string + size int + want bool + }{ + {"cap minus one", 1_048_575, true}, + {"cap", 1_048_576, true}, + {"cap plus one", 1_048_577, false}, + } { + t.Run(test.name, func(t *testing.T) { + parent := filepath.Join(t.TempDir(), "private") + if err := os.Mkdir(parent, 0o700); err != nil { + t.Fatal(err) + } + path := filepath.Join(parent, "session.json") + content := append([]byte("{}"), bytes.Repeat([]byte(" "), test.size-2)...) + if err := os.WriteFile(path, content, 0o600); err != nil { + t.Fatal(err) + } + store := NewAccountInfoFile(path) + _, ok := store.AuthData() + if test.want { + if !ok || store.Err() != nil { + t.Fatalf("AuthData() for %d-byte session: ok=%t err=%v, want loaded", test.size, ok, store.Err()) + } + } else if ok || !errors.Is(store.Err(), errUnavailableAccountInfoSession) { + t.Fatalf("AuthData() for %d-byte session: ok=%t err=%v, want size rejection", test.size, ok, store.Err()) + } + }) + } +} diff --git a/b2/accountinfo_test.go b/b2/accountinfo_test.go index 378c4ed..039223a 100644 --- a/b2/accountinfo_test.go +++ b/b2/accountinfo_test.go @@ -63,8 +63,8 @@ func TestAccountInfoFileRoundTripAndPermissions(t *testing.T) { if err != nil { t.Fatal(err) } - if !json.Valid(content) || strings.Contains(string(content), "application-key-secret") { - t.Fatal("file is not valid JSON or contains the application key") + if !json.Valid(content) { + t.Fatal("file is not valid JSON") } fileInfo, err := os.Stat(path) if err != nil { diff --git a/b2/core_edges_test.go b/b2/core_edges_test.go new file mode 100644 index 0000000..481d415 --- /dev/null +++ b/b2/core_edges_test.go @@ -0,0 +1,100 @@ +package b2 + +import ( + "context" + "strings" + "testing" + "time" +) + +func TestObjectLockUpdateInvalidatesCachedInfo(t *testing.T) { + for _, test := range []struct { + name string + update func(*Object) error + }{ + {"retention", func(o *Object) error { + return o.UpdateFileRetention(context.Background(), &FileRetention{Mode: RetentionGovernance, RetainUntil: time.UnixMilli(1_735_689_600_000)}, false) + }}, + {"legal hold", func(o *Object) error { + return o.UpdateFileLegalHold(context.Background(), LegalHoldOn) + }}, + } { + t.Run(test.name, func(t *testing.T) { + wire := newLockWire(t) + object := wire.object(t) + ctx := context.Background() + wire.info = `{"fileId":"id","fileName":"name","action":"upload","contentLength":1,"contentType":"text/plain","fileInfo":{"marker":"before"}}` + before, err := object.Attrs(ctx) + if err != nil { + t.Fatal(err) + } + if before.Info["marker"] != "before" { + t.Fatalf("initial file info marker = %q, want before", before.Info["marker"]) + } + calls := wire.calls + if err := test.update(object); err != nil { + t.Fatal(err) + } + wire.info = `{"fileId":"id","fileName":"name","action":"upload","contentLength":1,"contentType":"text/plain","fileInfo":{"marker":"after"}}` + after, err := object.Attrs(ctx) + if err != nil { + t.Fatal(err) + } + if after.Info["marker"] != "after" || wire.calls != calls+2 || wire.path != "/b2api/v4/b2_get_file_info" { + t.Fatalf("post-update marker=%q calls=%d (want %d) last path=%q", after.Info["marker"], wire.calls, calls+2, wire.path) + } + }) + } +} + +func TestObjectDeleteRejectsFolderStatus(t *testing.T) { + file := &testFile{n: "folder", a: "folder", files: map[string]string{"folder": "present"}} + object := objectForDeleteOptionTest(file) + if err := object.Delete(context.Background()); err == nil || !strings.Contains(err.Error(), "not a regular file or hide marker") { + t.Fatalf("Delete(folder) error = %v, want non-regular-file rejection", err) + } + if got := file.files["folder"]; got != "present" { + t.Fatalf("Delete(folder) changed file state to %q", got) + } +} + +func TestMemoryBufferCloseIsIdempotent(t *testing.T) { + buffer := newMemoryBuffer() + if _, err := buffer.Write([]byte("payload")); err != nil { + t.Fatal(err) + } + if err := buffer.Close(); err != nil { + t.Fatal(err) + } + if buffer.buf != nil { + t.Fatal("Close retained the buffer") + } + if err := buffer.Close(); err != nil { + t.Fatalf("second Close: %v", err) + } +} + +func TestProgressZeroWakeDoesNotDuplicateCallback(t *testing.T) { + var samples []int64 + p := &progressReporter{ + callback: func(completed, _ int64) { samples = append(samples, completed) }, + total: 1, + wake: make(chan struct{}), // A send acknowledges that run received the wake. + stop: make(chan struct{}), + done: make(chan struct{}), + } + go p.run() + p.add(1) + deadline := time.After(5 * time.Second) + for i := 0; i < 2; i++ { + select { + case p.wake <- struct{}{}: + case <-deadline: + t.Fatal("progress reporter did not receive a wake") + } + } + p.finish() + if len(samples) != 2 || samples[0] != 1 || samples[1] != 1 { + t.Fatalf("progress callbacks = %v, want one nonzero update and one terminal call", samples) + } +}