11import { Context , Effect , FileSystem , Layer , Schema } from "effect"
22import * as fs from "node:fs"
33import * as path from "node:path"
4- import { secretFindings , type SecretFinding } from "./redaction"
4+ import { secretFindings , SecretFindingSchema , type SecretFinding } from "./redaction"
55import { decodeCassette , encodeCassette , type Cassette , type CassetteMetadata , type Interaction } from "./schema"
66
77const DEFAULT_RECORDINGS_DIR = path . resolve ( process . cwd ( ) , "test" , "fixtures" , "recordings" )
@@ -14,13 +14,24 @@ export class CassetteNotFoundError extends Schema.TaggedErrorClass<CassetteNotFo
1414 }
1515}
1616
17- export interface AppendResult {
18- readonly findings : ReadonlyArray < SecretFinding >
17+ export class UnsafeCassetteError extends Schema . TaggedErrorClass < UnsafeCassetteError > ( ) ( "UnsafeCassetteError" , {
18+ cassetteName : Schema . String ,
19+ findings : Schema . Array ( SecretFindingSchema ) ,
20+ } ) {
21+ override get message ( ) {
22+ return `Refusing to write cassette "${ this . cassetteName } " because it contains possible secrets: ${ this . findings
23+ . map ( ( finding ) => `${ finding . path } (${ finding . reason } )` )
24+ . join ( ", " ) } `
25+ }
1926}
2027
2128export interface Interface {
2229 readonly read : ( name : string ) => Effect . Effect < ReadonlyArray < Interaction > , CassetteNotFoundError >
23- readonly append : ( name : string , interaction : Interaction , metadata ?: CassetteMetadata ) => Effect . Effect < AppendResult >
30+ readonly append : (
31+ name : string ,
32+ interaction : Interaction ,
33+ metadata ?: CassetteMetadata ,
34+ ) => Effect . Effect < void , UnsafeCassetteError >
2435 readonly exists : ( name : string ) => Effect . Effect < boolean >
2536 readonly list : ( ) => Effect . Effect < ReadonlyArray < string > >
2637}
@@ -44,6 +55,9 @@ const formatCassette = (cassette: Cassette) => `${JSON.stringify(encodeCassette(
4455
4556const parseCassette = ( raw : string ) => decodeCassette ( JSON . parse ( raw ) )
4657
58+ const failIfUnsafe = ( name : string , findings : ReadonlyArray < SecretFinding > ) =>
59+ findings . length === 0 ? Effect . void : Effect . fail ( new UnsafeCassetteError ( { cassetteName : name , findings } ) )
60+
4761export const fileSystem = (
4862 options : { readonly directory ?: string } = { } ,
4963) : Layer . Layer < Service , never , FileSystem . FileSystem > =>
@@ -92,11 +106,9 @@ export const fileSystem = (
92106 entry . findings . push ( ...secretFindings ( interaction ) )
93107 const cassette = buildCassette ( name , entry . interactions , metadata )
94108 const findings = [ ...entry . findings , ...secretFindings ( cassette . metadata ?? { } ) ]
95- if ( findings . length === 0 ) {
96- yield * ensureDirectory ( name )
97- yield * fs . writeFileString ( cassettePath ( name ) , formatCassette ( cassette ) ) . pipe ( Effect . orDie )
98- }
99- return { findings }
109+ yield * failIfUnsafe ( name , findings )
110+ yield * ensureDirectory ( name )
111+ yield * fs . writeFileString ( cassettePath ( name ) , formatCassette ( cassette ) ) . pipe ( Effect . orDie )
100112 } ) ,
101113 exists : ( name ) =>
102114 fs . access ( cassettePath ( name ) ) . pipe (
@@ -108,12 +120,7 @@ export const fileSystem = (
108120 Effect . map ( ( files ) =>
109121 files
110122 . filter ( ( file ) => file . endsWith ( ".json" ) )
111- . map ( ( file ) =>
112- path
113- . relative ( directory , file )
114- . replace ( / \\ / g, "/" )
115- . replace ( / \. j s o n $ / , "" ) ,
116- )
123+ . map ( ( file ) => path . relative ( directory , file ) . replace ( / \\ / g, "/" ) . replace ( / \. j s o n $ / , "" ) )
117124 . toSorted ( ( a , b ) => a . localeCompare ( b ) ) ,
118125 ) ,
119126 ) ,
@@ -133,17 +140,17 @@ export const memory = (initial: Record<string, ReadonlyArray<Interaction>> = {})
133140 stored . has ( name )
134141 ? Effect . succeed ( stored . get ( name ) ?? [ ] )
135142 : Effect . fail ( new CassetteNotFoundError ( { cassetteName : name } ) ) ,
136- append : ( name , interaction , metadata ) =>
137- Effect . sync ( ( ) => {
138- const existing = stored . get ( name )
139- if ( existing ) existing . push ( interaction )
140- else stored . set ( name , [ interaction ] )
141- const findings = accumulatedFindings . get ( name )
142- if ( findings ) findings . push ( ... secretFindings ( interaction ) )
143- else accumulatedFindings . set ( name , [ ...secretFindings ( interaction ) ] )
144- if ( metadata ) accumulatedFindings . get ( name ) ! . push ( ...secretFindings ( { name, ...metadata } ) )
145- return { findings : accumulatedFindings . get ( name ) ?? [ ] }
146- } ) ,
143+ append : ( name , interaction , metadata ) => {
144+ const existing = stored . get ( name )
145+ if ( existing ) existing . push ( interaction )
146+ else stored . set ( name , [ interaction ] )
147+ const existingFindings = accumulatedFindings . get ( name )
148+ const findings = existingFindings ?? [ ]
149+ if ( ! existingFindings ) accumulatedFindings . set ( name , findings )
150+ findings . push ( ...secretFindings ( interaction ) )
151+ if ( metadata ) findings . push ( ...secretFindings ( { name, ...metadata } ) )
152+ return failIfUnsafe ( name , findings )
153+ } ,
147154 exists : ( name ) => Effect . sync ( ( ) => stored . has ( name ) ) ,
148155 list : ( ) => Effect . sync ( ( ) => Array . from ( stored . keys ( ) ) . toSorted ( ) ) ,
149156 } )
0 commit comments