From 156b5c53f7bc7286c47ac2f83d7eac0500b7220d Mon Sep 17 00:00:00 2001 From: Houmgaor Date: Fri, 27 Feb 2026 13:07:12 +0100 Subject: [PATCH] test(signserver): push coverage from 62.9% to 70.3% Add handlePacket dispatch tests for all switch cases (DSGN, SIGN, DLTSKEYSIGN, PS4SGN, PS3SGN, VITASGN, WIIUSGN, COGLNK, VITACOGLNK, DELETE). Add makeSignResponse branch tests covering PSN client PSNID field, CapLink key/host paths, MezFes minigame switch, non-localhost remote addr, and PSN token registration. Add startSignCapture enabled-path tests with temp dir and default output dir. --- server/signserver/dsgn_resp_test.go | 308 +++++++++++++++++++++ server/signserver/repo_mocks_test.go | 1 - server/signserver/session_handlers_test.go | 242 ++++++++++++++++ server/signserver/sys_capture_test.go | 74 +++++ 4 files changed, 624 insertions(+), 1 deletion(-) diff --git a/server/signserver/dsgn_resp_test.go b/server/signserver/dsgn_resp_test.go index dce05f800..aeaf241e1 100644 --- a/server/signserver/dsgn_resp_test.go +++ b/server/signserver/dsgn_resp_test.go @@ -2,6 +2,7 @@ package signserver import ( "fmt" + "net" "strings" "testing" "time" @@ -263,3 +264,310 @@ func TestMakeSignResponse_FullFlow(t *testing.T) { t.Errorf("makeSignResponse() first byte = %d, want %d (SIGN_SUCCESS)", result[0], SIGN_SUCCESS) } } + +// TestMakeSignResponse_PSNClientWritesPSNID verifies PSN client appends PSNID field. +func TestMakeSignResponse_PSNClientWritesPSNID(t *testing.T) { + config := &cfg.Config{ + DebugOptions: cfg.DebugOptions{ + CapLink: cfg.CapLinkOptions{ + Values: []uint16{0, 0, 0, 0, 0}, + }, + }, + GameplayOptions: cfg.GameplayOptions{ + MezFesSoloTickets: 100, + MezFesGroupTickets: 100, + }, + } + + server := newMakeSignResponseServer(config) + server.charRepo = &mockSignCharacterRepo{ + characters: []character{{ID: 1, Name: "PSNHunter", HR: 50}}, + } + server.userRepo = &mockSignUserRepo{ + returnExpiry: time.Now().Add(time.Hour * 24 * 30), + lastLogin: time.Now(), + psnIDForUser: "MyPSNID", + } + + // PC response + pcConn := newMockConn() + pcSession := &Session{ + logger: zap.NewNop(), + server: server, + rawConn: pcConn, + client: PC100, + } + pcResult := pcSession.makeSignResponse(1) + + // PS4 response + ps4Conn := newMockConn() + ps4Session := &Session{ + logger: zap.NewNop(), + server: server, + rawConn: ps4Conn, + client: PS4, + } + ps4Result := ps4Session.makeSignResponse(1) + + // PSN response should be longer due to 20-byte PSNID field + if len(ps4Result) <= len(pcResult) { + t.Errorf("PS4 response len (%d) should be > PC response len (%d)", len(ps4Result), len(pcResult)) + } +} + +// TestMakeSignResponse_CapLink51728_20000 verifies CapLink key is written. +func TestMakeSignResponse_CapLink51728_20000(t *testing.T) { + config := &cfg.Config{ + DebugOptions: cfg.DebugOptions{ + CapLink: cfg.CapLinkOptions{ + Values: []uint16{51728, 20000, 0, 0, 0}, + Key: "caplink-key-test", + }, + }, + GameplayOptions: cfg.GameplayOptions{ + MezFesSoloTickets: 100, + MezFesGroupTickets: 100, + }, + } + + server := newMakeSignResponseServer(config) + conn := newMockConn() + session := &Session{ + logger: zap.NewNop(), + server: server, + rawConn: conn, + client: PC100, + } + + result := session.makeSignResponse(1) + if result[0] != uint8(SIGN_SUCCESS) { + t.Errorf("first byte = %d, want SIGN_SUCCESS", result[0]) + } + + // The response with CapLink key should be longer than without + configNoKey := &cfg.Config{ + DebugOptions: cfg.DebugOptions{ + CapLink: cfg.CapLinkOptions{ + Values: []uint16{0, 0, 0, 0, 0}, + }, + }, + GameplayOptions: cfg.GameplayOptions{ + MezFesSoloTickets: 100, + MezFesGroupTickets: 100, + }, + } + serverNoKey := newMakeSignResponseServer(configNoKey) + sessionNoKey := &Session{ + logger: zap.NewNop(), + server: serverNoKey, + rawConn: newMockConn(), + client: PC100, + } + resultNoKey := sessionNoKey.makeSignResponse(1) + + if len(result) <= len(resultNoKey) { + t.Errorf("CapLink 51728/20000 response len (%d) should be > base response len (%d)", len(result), len(resultNoKey)) + } +} + +// TestMakeSignResponse_CapLink51728_20002 verifies the 20002 variant also writes key. +func TestMakeSignResponse_CapLink51728_20002(t *testing.T) { + config := &cfg.Config{ + DebugOptions: cfg.DebugOptions{ + CapLink: cfg.CapLinkOptions{ + Values: []uint16{51728, 20002, 0, 0, 0}, + Key: "caplink-key-20002", + }, + }, + GameplayOptions: cfg.GameplayOptions{ + MezFesSoloTickets: 100, + MezFesGroupTickets: 100, + }, + } + + server := newMakeSignResponseServer(config) + session := &Session{ + logger: zap.NewNop(), + server: server, + rawConn: newMockConn(), + client: PC100, + } + + result := session.makeSignResponse(1) + if result[0] != uint8(SIGN_SUCCESS) { + t.Errorf("first byte = %d, want SIGN_SUCCESS", result[0]) + } + if len(result) == 0 { + t.Error("makeSignResponse() returned empty result") + } +} + +// TestMakeSignResponse_CapLink51729Combo verifies the 51729 host:port write. +func TestMakeSignResponse_CapLink51729Combo(t *testing.T) { + config := &cfg.Config{ + DebugOptions: cfg.DebugOptions{ + CapLink: cfg.CapLinkOptions{ + Values: []uint16{0, 0, 51729, 1, 20000}, + Host: "caplink.example.com", + Port: 9999, + }, + }, + GameplayOptions: cfg.GameplayOptions{ + MezFesSoloTickets: 100, + MezFesGroupTickets: 100, + }, + } + + server := newMakeSignResponseServer(config) + session := &Session{ + logger: zap.NewNop(), + server: server, + rawConn: newMockConn(), + client: PC100, + } + + result := session.makeSignResponse(1) + if result[0] != uint8(SIGN_SUCCESS) { + t.Errorf("first byte = %d, want SIGN_SUCCESS", result[0]) + } + + // Response with 51729 combo should include host:port string, making it longer + configNoCap := &cfg.Config{ + DebugOptions: cfg.DebugOptions{ + CapLink: cfg.CapLinkOptions{ + Values: []uint16{0, 0, 0, 0, 0}, + }, + }, + GameplayOptions: cfg.GameplayOptions{ + MezFesSoloTickets: 100, + MezFesGroupTickets: 100, + }, + } + serverNoCap := newMakeSignResponseServer(configNoCap) + sessionNoCap := &Session{ + logger: zap.NewNop(), + server: serverNoCap, + rawConn: newMockConn(), + client: PC100, + } + resultNoCap := sessionNoCap.makeSignResponse(1) + + if len(result) <= len(resultNoCap) { + t.Errorf("CapLink 51729 combo response len (%d) should be > base len (%d)", len(result), len(resultNoCap)) + } +} + +// TestMakeSignResponse_MezFesSwitchMinigame verifies stalls[4] is set to 2. +func TestMakeSignResponse_MezFesSwitchMinigame(t *testing.T) { + config := &cfg.Config{ + DebugOptions: cfg.DebugOptions{ + CapLink: cfg.CapLinkOptions{ + Values: []uint16{0, 0, 0, 0, 0}, + }, + }, + GameplayOptions: cfg.GameplayOptions{ + MezFesSoloTickets: 100, + MezFesGroupTickets: 100, + MezFesSwitchMinigame: true, + }, + } + + server := newMakeSignResponseServer(config) + session := &Session{ + logger: zap.NewNop(), + server: server, + rawConn: newMockConn(), + client: PC100, + } + + result := session.makeSignResponse(1) + if result[0] != uint8(SIGN_SUCCESS) { + t.Errorf("first byte = %d, want SIGN_SUCCESS", result[0]) + } + if len(result) == 0 { + t.Error("makeSignResponse() returned empty result") + } +} + +// mockConnRemote is a mockConn variant with a configurable RemoteAddr. +type mockConnRemote struct { + mockConn + remoteAddr net.Addr +} + +func (m *mockConnRemote) RemoteAddr() net.Addr { + return m.remoteAddr +} + +// TestMakeSignResponse_NonLocalhostRemote verifies the non-localhost entrance addr path. +func TestMakeSignResponse_NonLocalhostRemote(t *testing.T) { + config := &cfg.Config{ + Host: "192.168.1.100", + Entrance: cfg.Entrance{ + Port: 53310, + }, + DebugOptions: cfg.DebugOptions{ + CapLink: cfg.CapLinkOptions{ + Values: []uint16{0, 0, 0, 0, 0}, + }, + }, + GameplayOptions: cfg.GameplayOptions{ + MezFesSoloTickets: 100, + MezFesGroupTickets: 100, + }, + } + + server := newMakeSignResponseServer(config) + conn := &mockConnRemote{ + mockConn: *newMockConn(), + remoteAddr: &net.TCPAddr{IP: net.ParseIP("10.0.0.5"), Port: 12345}, + } + session := &Session{ + logger: zap.NewNop(), + server: server, + rawConn: conn, + client: PC100, + } + + result := session.makeSignResponse(1) + if result[0] != uint8(SIGN_SUCCESS) { + t.Errorf("first byte = %d, want SIGN_SUCCESS", result[0]) + } + // Response should contain the Host address (192.168.1.100) rather than 127.0.0.1 + resultStr := string(result) + if !strings.Contains(resultStr, "192.168.1.100") { + t.Error("non-localhost response should contain Host address") + } +} + +// TestMakeSignResponse_PSNTokenPath verifies the PSN token registration when uid=0 with psn set. +func TestMakeSignResponse_PSNTokenPath(t *testing.T) { + config := &cfg.Config{ + DebugOptions: cfg.DebugOptions{ + CapLink: cfg.CapLinkOptions{ + Values: []uint16{0, 0, 0, 0, 0}, + }, + }, + GameplayOptions: cfg.GameplayOptions{ + MezFesSoloTickets: 100, + MezFesGroupTickets: 100, + }, + } + + server := newMakeSignResponseServer(config) + server.sessionRepo = &mockSignSessionRepo{ + registerPSNTokenID: 500, + } + session := &Session{ + logger: zap.NewNop(), + server: server, + rawConn: newMockConn(), + client: PS4, + psn: "my_psn_id", + } + + result := session.makeSignResponse(0) + if result[0] != uint8(SIGN_SUCCESS) { + t.Errorf("first byte = %d, want SIGN_SUCCESS", result[0]) + } +} diff --git a/server/signserver/repo_mocks_test.go b/server/signserver/repo_mocks_test.go index ef3b83d1a..73e813982 100644 --- a/server/signserver/repo_mocks_test.go +++ b/server/signserver/repo_mocks_test.go @@ -252,4 +252,3 @@ func (m *mockSignSessionRepo) Validate(token string, tokenID uint32) (bool, erro func (m *mockSignSessionRepo) GetPSNIDByToken(token string) (string, error) { return m.psnIDByToken, m.psnIDByTokenErr } - diff --git a/server/signserver/session_handlers_test.go b/server/signserver/session_handlers_test.go index 1e1614df7..4caf14efe 100644 --- a/server/signserver/session_handlers_test.go +++ b/server/signserver/session_handlers_test.go @@ -846,3 +846,245 @@ func TestAuthenticate_RegisterTokenError(t *testing.T) { t.Errorf("authenticate() token error first byte = %d, want SIGN_EABORT(%d)", pkt[0], SIGN_EABORT) } } + +// --- handlePacket dispatch --- + +func TestHandlePacket_DSGN(t *testing.T) { + userRepo := &mockSignUserRepo{credErr: sql.ErrNoRows} + session, spy := newHandlerSession(userRepo, nil, nil, defaultConfig()) + + bf := byteframe.NewByteFrame() + bf.WriteNullTerminatedBytes([]byte("DSGN:100")) + bf.WriteNullTerminatedBytes([]byte("user")) + bf.WriteNullTerminatedBytes([]byte("pass")) + bf.WriteNullTerminatedBytes([]byte("unk")) + + err := session.handlePacket(bf.Data()) + if err != nil { + t.Fatalf("handlePacket(DSGN) error: %v", err) + } + if spy.lastSent() == nil { + t.Fatal("handlePacket(DSGN) sent no packet") + } +} + +func TestHandlePacket_SIGN(t *testing.T) { + userRepo := &mockSignUserRepo{credErr: sql.ErrNoRows} + session, spy := newHandlerSession(userRepo, nil, nil, defaultConfig()) + + bf := byteframe.NewByteFrame() + bf.WriteNullTerminatedBytes([]byte("SIGN:100")) + bf.WriteNullTerminatedBytes([]byte("user")) + bf.WriteNullTerminatedBytes([]byte("pass")) + bf.WriteNullTerminatedBytes([]byte("unk")) + + err := session.handlePacket(bf.Data()) + if err != nil { + t.Fatalf("handlePacket(SIGN) error: %v", err) + } + if spy.lastSent() == nil { + t.Fatal("handlePacket(SIGN) sent no packet") + } +} + +func TestHandlePacket_DLTSKEYSIGN(t *testing.T) { + userRepo := &mockSignUserRepo{credErr: sql.ErrNoRows} + session, spy := newHandlerSession(userRepo, nil, nil, defaultConfig()) + + bf := byteframe.NewByteFrame() + bf.WriteNullTerminatedBytes([]byte("DLTSKEYSIGN:100")) + bf.WriteNullTerminatedBytes([]byte("user")) + bf.WriteNullTerminatedBytes([]byte("pass")) + bf.WriteNullTerminatedBytes([]byte("unk")) + + err := session.handlePacket(bf.Data()) + if err != nil { + t.Fatalf("handlePacket(DLTSKEYSIGN) error: %v", err) + } + if spy.lastSent() == nil { + t.Fatal("handlePacket(DLTSKEYSIGN) sent no packet") + } +} + +func TestHandlePacket_PS4SGN(t *testing.T) { + userRepo := &mockSignUserRepo{psnUID: 10} + charRepo := &mockSignCharacterRepo{characters: []character{{ID: 1, Name: "PS4Char"}}} + sessionRepo := &mockSignSessionRepo{registerUIDTokenID: 100} + session, spy := newHandlerSession(userRepo, charRepo, sessionRepo, defaultConfig()) + + bf := byteframe.NewByteFrame() + bf.WriteNullTerminatedBytes([]byte("PS4SGN:100")) + bf.WriteNullTerminatedBytes([]byte("ps4_psn_id")) + + err := session.handlePacket(bf.Data()) + if err != nil { + t.Fatalf("handlePacket(PS4SGN) error: %v", err) + } + if session.client != PS4 { + t.Errorf("handlePacket(PS4SGN) client = %d, want PS4(%d)", session.client, PS4) + } + if spy.lastSent() == nil { + t.Fatal("handlePacket(PS4SGN) sent no packet") + } +} + +func TestHandlePacket_PS3SGN(t *testing.T) { + // PS3 with short buffer should abort + session, spy := newHandlerSession(nil, nil, nil, defaultConfig()) + + bf := byteframe.NewByteFrame() + bf.WriteNullTerminatedBytes([]byte("PS3SGN:100")) + bf.WriteBytes(make([]byte, 10)) // too short for PS3 + + err := session.handlePacket(bf.Data()) + if err != nil { + t.Fatalf("handlePacket(PS3SGN) error: %v", err) + } + if session.client != PS3 { + t.Errorf("handlePacket(PS3SGN) client = %d, want PS3(%d)", session.client, PS3) + } + pkt := spy.lastSent() + if pkt == nil { + t.Fatal("handlePacket(PS3SGN) sent no packet") + } + if len(pkt) != 1 || RespID(pkt[0]) != SIGN_EABORT { + t.Errorf("handlePacket(PS3SGN) short buffer = %v, want SIGN_EABORT", pkt) + } +} + +func TestHandlePacket_VITASGN(t *testing.T) { + // VITA with short buffer should abort + session, spy := newHandlerSession(nil, nil, nil, defaultConfig()) + + bf := byteframe.NewByteFrame() + bf.WriteNullTerminatedBytes([]byte("VITASGN:100")) + bf.WriteBytes(make([]byte, 10)) // too short + + err := session.handlePacket(bf.Data()) + if err != nil { + t.Fatalf("handlePacket(VITASGN) error: %v", err) + } + if session.client != VITA { + t.Errorf("handlePacket(VITASGN) client = %d, want VITA(%d)", session.client, VITA) + } + pkt := spy.lastSent() + if pkt == nil { + t.Fatal("handlePacket(VITASGN) sent no packet") + } + if len(pkt) != 1 || RespID(pkt[0]) != SIGN_EABORT { + t.Errorf("handlePacket(VITASGN) short buffer = %v, want SIGN_EABORT", pkt) + } +} + +func TestHandlePacket_WIIUSGN(t *testing.T) { + userRepo := &mockSignUserRepo{wiiuUID: 10} + charRepo := &mockSignCharacterRepo{characters: []character{{ID: 1, Name: "WiiUChar"}}} + sessionRepo := &mockSignSessionRepo{registerUIDTokenID: 200} + session, spy := newHandlerSession(userRepo, charRepo, sessionRepo, defaultConfig()) + + bf := byteframe.NewByteFrame() + bf.WriteNullTerminatedBytes([]byte("WIIUSGN:100")) + bf.WriteBytes(make([]byte, 1)) // skip byte + bf.WriteBytes(make([]byte, 64)) // wiiuKey + + err := session.handlePacket(bf.Data()) + if err != nil { + t.Fatalf("handlePacket(WIIUSGN) error: %v", err) + } + if session.client != WIIU { + t.Errorf("handlePacket(WIIUSGN) client = %d, want WIIU(%d)", session.client, WIIU) + } + if spy.lastSent() == nil { + t.Fatal("handlePacket(WIIUSGN) sent no packet") + } +} + +func TestHandlePacket_COGLNK(t *testing.T) { + userRepo := &mockSignUserRepo{credErr: sql.ErrNoRows} + session, spy := newHandlerSession(userRepo, nil, nil, defaultConfig()) + + bf := byteframe.NewByteFrame() + bf.WriteNullTerminatedBytes([]byte("COGLNK:100")) + bf.WriteNullTerminatedBytes([]byte("client_id")) + bf.WriteNullTerminatedBytes([]byte("baduser\nbadpass")) + bf.WriteNullTerminatedBytes([]byte("token")) + + err := session.handlePacket(bf.Data()) + if err != nil { + t.Fatalf("handlePacket(COGLNK) error: %v", err) + } + pkt := spy.lastSent() + if pkt == nil { + t.Fatal("handlePacket(COGLNK) sent no packet") + } + // Invalid creds → SIGN_ECOGLINK + if len(pkt) != 1 || RespID(pkt[0]) != SIGN_ECOGLINK { + t.Errorf("handlePacket(COGLNK) = %v, want SIGN_ECOGLINK", pkt) + } +} + +func TestHandlePacket_VITACOGLNK(t *testing.T) { + userRepo := &mockSignUserRepo{credErr: sql.ErrNoRows} + session, spy := newHandlerSession(userRepo, nil, nil, defaultConfig()) + + bf := byteframe.NewByteFrame() + bf.WriteNullTerminatedBytes([]byte("VITACOGLNK:100")) + bf.WriteNullTerminatedBytes([]byte("client_id")) + bf.WriteNullTerminatedBytes([]byte("user\npass")) + bf.WriteNullTerminatedBytes([]byte("token")) + + err := session.handlePacket(bf.Data()) + if err != nil { + t.Fatalf("handlePacket(VITACOGLNK) error: %v", err) + } + if spy.lastSent() == nil { + t.Fatal("handlePacket(VITACOGLNK) sent no packet") + } +} + +func TestHandlePacket_DELETE(t *testing.T) { + charRepo := &mockSignCharacterRepo{isNew: true} + sessionRepo := &mockSignSessionRepo{validateResult: true} + session, spy := newHandlerSession(nil, charRepo, sessionRepo, defaultConfig()) + + bf := byteframe.NewByteFrame() + bf.WriteNullTerminatedBytes([]byte("DELETE:100")) + bf.WriteNullTerminatedBytes([]byte("sesstoken")) + bf.WriteUint32(42) // characterID + bf.WriteUint32(1) // tokenID + + err := session.handlePacket(bf.Data()) + if err != nil { + t.Fatalf("handlePacket(DELETE) error: %v", err) + } + pkt := spy.lastSent() + if pkt == nil { + t.Fatal("handlePacket(DELETE) sent no packet") + } + if pkt[0] != 0x01 { + t.Errorf("handlePacket(DELETE) = %x, want 0x01 (DEL_SUCCESS)", pkt[0]) + } + if !charRepo.hardDeleteCalled { + t.Error("handlePacket(DELETE) should call HardDelete for new character") + } +} + +func TestHandlePacket_DELETE_InvalidToken(t *testing.T) { + sessionRepo := &mockSignSessionRepo{validateResult: false} + session, spy := newHandlerSession(nil, nil, sessionRepo, defaultConfig()) + + bf := byteframe.NewByteFrame() + bf.WriteNullTerminatedBytes([]byte("DELETE:100")) + bf.WriteNullTerminatedBytes([]byte("badtoken")) + bf.WriteUint32(42) // characterID + bf.WriteUint32(1) // tokenID + + err := session.handlePacket(bf.Data()) + if err != nil { + t.Fatalf("handlePacket(DELETE) error: %v", err) + } + // Invalid token → deleteCharacter returns error → no packet sent + if spy.lastSent() != nil { + t.Error("handlePacket(DELETE) with invalid token should not send DEL_SUCCESS") + } +} diff --git a/server/signserver/sys_capture_test.go b/server/signserver/sys_capture_test.go index 8d672c5c2..49f593508 100644 --- a/server/signserver/sys_capture_test.go +++ b/server/signserver/sys_capture_test.go @@ -2,6 +2,7 @@ package signserver import ( "net" + "os" "testing" cfg "erupe-ce/config" @@ -80,3 +81,76 @@ func TestStartSignCapture_EnabledButSignDisabled(t *testing.T) { } cleanup() } + +func TestStartSignCapture_EnabledSuccess(t *testing.T) { + outputDir := t.TempDir() + server := &Server{ + logger: zap.NewNop(), + erupeConfig: &cfg.Config{ + Host: "127.0.0.1", + Sign: cfg.Sign{Port: 53312}, + Capture: cfg.CaptureOptions{ + Enabled: true, + CaptureSign: true, + OutputDir: outputDir, + }, + }, + } + + mc := newMockConn() + origConn := network.NewCryptConn(mc, cfg.ZZ, nil) + remoteAddr := &net.TCPAddr{IP: net.ParseIP("127.0.0.1"), Port: 12345} + + resultConn, cleanup := startSignCapture(server, origConn, remoteAddr) + defer cleanup() + + if resultConn == origConn { + t.Error("startSignCapture() enabled should return a different (recording) conn") + } +} + +func TestStartSignCapture_DefaultOutputDir(t *testing.T) { + // Use a temp dir as working directory to avoid polluting the project + origDir, err := os.Getwd() + if err != nil { + t.Fatal(err) + } + tmpDir := t.TempDir() + if err := os.Chdir(tmpDir); err != nil { + t.Fatal(err) + } + defer func() { _ = os.Chdir(origDir) }() + + server := &Server{ + logger: zap.NewNop(), + erupeConfig: &cfg.Config{ + Host: "127.0.0.1", + Sign: cfg.Sign{Port: 53312}, + Capture: cfg.CaptureOptions{ + Enabled: true, + CaptureSign: true, + OutputDir: "", // empty → should default to "captures" + }, + }, + } + + mc := newMockConn() + origConn := network.NewCryptConn(mc, cfg.ZZ, nil) + remoteAddr := &net.TCPAddr{IP: net.ParseIP("127.0.0.1"), Port: 12345} + + resultConn, cleanup := startSignCapture(server, origConn, remoteAddr) + defer cleanup() + + if resultConn == origConn { + t.Error("startSignCapture() with default dir should return recording conn") + } + + // Verify the "captures" directory was created + info, err := os.Stat("captures") + if err != nil { + t.Fatalf("default 'captures' directory not created: %v", err) + } + if !info.IsDir() { + t.Error("'captures' should be a directory") + } +}