package streamdata_test import ( "encoding/json" "errors" "io" "os" "path" "reflect" "slices" "syscall" "testing" "time" "git.gensokyo.uk/yonah/streamdata" ) func TestChannelInvalid(t *testing.T) { t.Parallel() if err := (*streamdata.Channel)(nil).Create(""); !reflect.DeepEqual(err, syscall.EINVAL) { t.Fatalf("Create: error = %v", err) } if err := (*streamdata.Channel)(nil).Close(); !reflect.DeepEqual(err, syscall.EINVAL) { t.Fatalf("Close: error = %v", err) } } func TestChannelOpenPerm(t *testing.T) { t.Parallel() d := t.TempDir() if err := os.Chmod(d, 0); err != nil { t.Fatal(err) } wantErr := &os.PathError{ Op: "open", Path: d, Err: syscall.EACCES, } t.Run("open", func(t *testing.T) { t.Parallel() if _, err := streamdata.Open(d); !reflect.DeepEqual(err, wantErr) { t.Fatalf("Open: error = %#v, want %#v", err, wantErr) } }) t.Run("create", func(t *testing.T) { t.Parallel() t.Run("root", func(t *testing.T) { t.Parallel() if err := (&streamdata.Channel{ Identifier: 0xcafe, Name: ":3", }).Create(d); !reflect.DeepEqual(err, wantErr) { t.Fatalf("Create: error = %#v, want %#v", err, wantErr) } }) t.Run("mkdir", func(t *testing.T) { t.Parallel() wantErrMkdir := &os.PathError{ Op: "mkdir", Path: path.Join(d, "stream"), Err: syscall.EACCES, } if err := (&streamdata.Channel{ Identifier: 0xcafe, Name: ":3", }).Create(path.Join(d, "stream")); !reflect.DeepEqual(err, wantErrMkdir) { t.Fatalf("Create: error = %#v, want %#v", err, wantErrMkdir) } }) t.Run("metadata", func(t *testing.T) { t.Parallel() nx := t.TempDir() if err := os.Chmod(nx, 0400); err != nil { t.Fatal(err) } wantErrMetadata := &os.PathError{ Op: "openat", Path: "channel", Err: syscall.EACCES, } if err := (&streamdata.Channel{ Identifier: 0xcafe, Name: ":3", }).Create(nx); !reflect.DeepEqual(err, wantErrMetadata) { t.Fatalf("Create: error = %#v, want %#v", err, wantErrMetadata) } }) }) } func TestChannelCreate(t *testing.T) { t.Parallel() d := t.TempDir() c := streamdata.Channel{ Identifier: 0xcafe, Name: ":3", } if err := c.Create(d); err != nil { t.Fatalf("Create: error = %v", err) } if err := c.Close(); err != nil { t.Fatalf("Close: error = %v", err) } if got, err := streamdata.Open(d); err != nil { t.Fatalf("Open: error = %v", err) } else if err = got.Close(); err != nil { t.Fatalf("Close: error = %v", err) } else if !got.Is(&c) { t.Errorf("Open: %#v, want %#v", got, c) } } func TestChannelDangling(t *testing.T) { t.Parallel() t.Run("dangling", func(t *testing.T) { t.Parallel() d := t.TempDir() if err := os.WriteFile(path.Join(d, "pending"), nil, 0); err != nil { t.Fatal(err) } if _, err := streamdata.Open(d); !reflect.DeepEqual(err, streamdata.ErrDanglingTransaction) { t.Errorf("Open: error = %v", err) } }) t.Run("perm", func(t *testing.T) { t.Parallel() d := t.TempDir() if err := os.Chmod(d, 0400); err != nil { t.Fatal(err) } wantErr := &os.PathError{ Op: "statat", Path: "pending", Err: syscall.EACCES, } if _, err := streamdata.Open(d); !reflect.DeepEqual(err, wantErr) { t.Errorf("Open: error = %#v, want %#v", err, wantErr) } }) } func TestChannelBadMetadata(t *testing.T) { t.Run("perm", func(t *testing.T) { t.Parallel() d := t.TempDir() if err := os.WriteFile(path.Join(d, "channel"), nil, 0); err != nil { t.Fatal(err) } wantErr := &os.PathError{ Op: "openat", Path: "channel", Err: syscall.EACCES, } if _, err := streamdata.Open(d); !reflect.DeepEqual(err, wantErr) { t.Errorf("Open: error = %#v, want %#v", err, wantErr) } }) t.Run("invalid", func(t *testing.T) { t.Parallel() d := t.TempDir() if err := os.WriteFile(path.Join(d, "channel"), nil, 0400); err != nil { t.Fatal(err) } wantErr := io.EOF if _, err := streamdata.Open(d); !reflect.DeepEqual(err, wantErr) { t.Errorf("Open: error = %#v, want %#v", err, wantErr) } }) } func TestChannel(t *testing.T) { t.Parallel() d := t.TempDir() c := streamdata.Channel{ Identifier: 0xcafe, Name: ":3", } if err := c.Create(d); err != nil { t.Fatalf("Create: error = %v", err) } t.Cleanup(func() { if err := c.Close(); err != nil { t.Fatalf("Close: error = %v", err) } }) var wantErr error if err := c.Add(nil, nil); !reflect.DeepEqual(err, syscall.EINVAL) { t.Errorf("(invalid) Add: error = %#v", err) } wantErr = &streamdata.ChannelMismatchError{ Got: 0xbabe, Want: 0xcafe, } if err := c.Add(&streamdata.Ident{ Channel: 0xbabe, }, func(*streamdata.VOD, io.Writer) error { panic("unreachable") }); !reflect.DeepEqual(err, wantErr) { t.Fatalf("(channel) Add: error = %#v, want %#v", err, wantErr) } wantErr = &os.PathError{ Op: "openat", Path: "pending", Err: syscall.EEXIST, } if err := os.WriteFile(path.Join(d, "pending"), nil, 0); err != nil { t.Fatal(err) } if err := c.Add(&streamdata.Ident{ Channel: 0xcafe, }, func(*streamdata.VOD, io.Writer) error { panic("unreachable") }); !reflect.DeepEqual(err, wantErr) { t.Fatalf("(create) Add: error = %#v, want %#v", err, wantErr) } if err := os.Remove(path.Join(d, "pending")); err != nil { t.Fatal(err) } wantErr = errors.New("unique error") if err := c.Add(&streamdata.Ident{ Channel: 0xcafe, }, func(*streamdata.VOD, io.Writer) error { return wantErr }); !reflect.DeepEqual(err, wantErr) { t.Fatalf("(callback) Add: error = %#v, want %#v", err, wantErr) } if _, err := os.Stat(path.Join(d, "pending")); !errors.Is(err, os.ErrNotExist) { t.Fatalf("Stat: error = %v", err) } wantData := []byte{0xde, 0xad, 0xbe, 0xef} wantVOD := streamdata.VOD{ Title: "\x00", Date: time.Unix(0, 0).UTC(), Category: "\t", } ident := streamdata.Ident{ Serial: 0xfdfdfdfd, Channel: 0xcafe, Data: [streamdata.IdentFFLen]byte{ 0xfe, 0xe1, 0xde, 0xad, 0xfe, 0xed, 0x0b, 0xad, 0xf0, 0x0d, 0, 0, 0, 0, 0, 0, }, } if err := c.Add(&ident, func(v *streamdata.VOD, w io.Writer) error { *v = wantVOD _, err := w.Write(wantData) return err }); err != nil { t.Fatalf("Add: error = %v", err) } if dents, err := os.ReadDir(path.Join(d, "vod")); err != nil { t.Fatal(err) } else if len(dents) != 2 { t.Fatalf("ReadDir: %#v", dents) } const wantIdent = "4261281277-51966-fee1dead-feed-0bad-f00d-000000000000" var got streamdata.VOD if r, err := os.Open(path.Join(d, "vod", wantIdent)); err != nil { t.Fatal(err) } else if err = json.NewDecoder(r).Decode(&got); err != nil { _ = r.Close() t.Fatalf("Decode: error = %v", err) } else if got != wantVOD { t.Errorf("Add: %#v, want %#v", got, wantVOD) } var iterErr error idents := slices.Collect(c.All(&iterErr, false)) if iterErr != nil { t.Fatalf("All: error = %#v", iterErr) } if len(idents) != 1 || idents[0].String() != wantIdent { t.Errorf("All: %#v", idents) } if gotVOD, err := c.Load(&ident); err != nil { t.Fatalf("Load: error = %v", err) } else if *gotVOD != wantVOD { t.Errorf("Load: %#v, want %#v", *gotVOD, wantVOD) } if gotData, err := os.ReadFile(path.Join(d, "vod", wantIdent+streamdata.ChannelVODSuffix)); err != nil { t.Fatal(err) } else if string(gotData) != string(wantData) { t.Errorf("Add: data = %#v, want %#v", gotData, wantData) } if fi, err := os.Stat(path.Join(d, "vod", wantIdent)); err != nil { t.Fatal(err) } else if fi.Mode().Perm() != 0444 { t.Errorf("Perm: %#o", fi.Mode().Perm()) } if fi, err := os.Stat(path.Join(d, "vod", wantIdent+streamdata.ChannelVODSuffix)); err != nil { t.Fatal(err) } else if fi.Mode().Perm() != 0444 { t.Errorf("Perm: %#o", fi.Mode().Perm()) } rf := streamdata.RenameFrom(path.Join(d, "alternate")) if err := os.WriteFile(string(rf), []byte{0}, 0); err != nil { t.Fatal(err) } if err := c.Add(&streamdata.Ident{Channel: 0xcafe}, func(*streamdata.VOD, io.Writer) error { return rf }); err != nil { t.Fatalf("Add: error = %v", err) } if _, err := os.Stat(string(rf)); !errors.Is(err, os.ErrNotExist) { t.Fatalf("Stat: error = %v", err) } if data, err := os.ReadFile(path.Join( d, "vod", (&streamdata.Ident{Channel: 0xcafe}).String()+".mp4", )); err != nil { t.Fatal(err) } else if string(data) != "\x00" { t.Errorf("(rf) Add: %#v", data) } wantVODEdit := streamdata.VOD{ Title: "edit\x00", Date: time.Unix(0xdeadbeef, 0).UTC(), Category: "\t\x00", Mirror: "https://satori.hifuu.internal/edit.mp4\x00", } if err := c.Edit(&streamdata.Ident{Channel: 0xcafe}, func(v *streamdata.VOD) error { if *v != (streamdata.VOD{}) { t.Errorf("Edit: v = %#v", v) } *v = wantVODEdit return nil }); err != nil { t.Fatalf("Edit: error = %v", err) } if gotVODEdit, err := c.Load(&streamdata.Ident{Channel: 0xcafe}); err != nil { t.Fatalf("(edit) Load: error = %v", err) } else if *gotVODEdit != wantVODEdit { t.Errorf("Edit: %#v, want %#v", *gotVODEdit, wantVODEdit) } if err := os.Chmod(path.Join(d, "vod", wantIdent), 0); err != nil { t.Fatal(err) } wantErr = &os.PathError{ Op: "openat", Path: path.Join("vod", wantIdent), Err: syscall.EACCES, } if _, err := c.Load(&ident); !reflect.DeepEqual(err, wantErr) { t.Fatalf("(perm) Load: error = %#v, want %#v", err, wantErr) } if err := os.Chmod(path.Join(d, "vod", wantIdent), 0600); err != nil { t.Fatal(err) } else if err = os.WriteFile(path.Join(d, "vod", wantIdent), nil, 0); err != nil { t.Fatal(err) } wantErr = io.EOF if _, err := c.Load(&ident); !reflect.DeepEqual(err, wantErr) { t.Fatalf("(invalid) Load: error = %#v, want %#v", err, wantErr) } } func TestErrors(t *testing.T) { t.Parallel() testCases := []struct { name string err error want string }{ {"IdentFieldError", streamdata.IdentFieldError(0xcafe), "got 51966 field(s) instead of 7"}, {"IdentFFError", &streamdata.IdentFFError{Got: 0xcafe, Want: 0xbabe}, "got 51966 bytes for a 47806-byte long free-form field"}, {"ChannelMismatchError", &streamdata.ChannelMismatchError{Got: 0xcafe, Want: 0xbabe}, "attempting to add VOD from channel 51966 to channel 47806"}, } for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { t.Parallel() if got := tc.err.Error(); got != tc.want { t.Errorf("Error: %q, want %q", got, tc.want) } }) } }