├── .gitignore
├── .travis.yml
├── HttpSimulator.Tests
├── HttpSimulator.Tests.csproj
├── Test.cs
└── packages.config
├── HttpSimulator.sln
├── HttpSimulator
├── BaseWrapped
│ ├── HttpContext.cs
│ ├── HttpServerUtility.cs
│ ├── HttpSessionState.cs
│ └── SimulatedHttpRequest.cs
├── HttpSimulator.cs
├── HttpSimulator.csproj
├── IHttpContext.cs
├── IHttpRequest.cs
├── IHttpResponse.cs
├── IHttpServerUtility.cs
├── Properties
│ └── AssemblyInfo.cs
├── ReflectionHelper.cs
└── SimulatedHttpRequest.cs
├── LICENSE
└── README.md
/.gitignore:
--------------------------------------------------------------------------------
1 |
2 | # Created by https://www.gitignore.io/api/monodevelop,visualstudio,visualstudiocode
3 | # Edit at https://www.gitignore.io/?templates=monodevelop,visualstudio,visualstudiocode
4 |
5 | ### MonoDevelop ###
6 | #User Specific
7 | *.userprefs
8 | *.usertasks
9 |
10 | #Mono Project Files
11 | *.pidb
12 | *.resources
13 | test-results/
14 |
15 | ### VisualStudioCode ###
16 | .vscode/*
17 | !.vscode/settings.json
18 | !.vscode/tasks.json
19 | !.vscode/launch.json
20 | !.vscode/extensions.json
21 |
22 | ### VisualStudioCode Patch ###
23 | # Ignore all local history of files
24 | .history
25 |
26 | ### VisualStudio ###
27 | ## Ignore Visual Studio temporary files, build results, and
28 | ## files generated by popular Visual Studio add-ons.
29 | ##
30 | ## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore
31 |
32 | # User-specific files
33 | *.rsuser
34 | *.suo
35 | *.user
36 | *.userosscache
37 | *.sln.docstates
38 |
39 | # User-specific files (MonoDevelop/Xamarin Studio)
40 |
41 | # Mono auto generated files
42 | mono_crash.*
43 |
44 | # Build results
45 | [Dd]ebug/
46 | [Dd]ebugPublic/
47 | [Rr]elease/
48 | [Rr]eleases/
49 | x64/
50 | x86/
51 | [Aa][Rr][Mm]/
52 | [Aa][Rr][Mm]64/
53 | bld/
54 | [Bb]in/
55 | [Oo]bj/
56 | [Ll]og/
57 |
58 | # Visual Studio 2015/2017 cache/options directory
59 | .vs/
60 | # Uncomment if you have tasks that create the project's static files in wwwroot
61 | #wwwroot/
62 |
63 | # Visual Studio 2017 auto generated files
64 | Generated\ Files/
65 |
66 | # MSTest test Results
67 | [Tt]est[Rr]esult*/
68 | [Bb]uild[Ll]og.*
69 |
70 | # NUnit
71 | *.VisualState.xml
72 | TestResult.xml
73 | nunit-*.xml
74 |
75 | # Build Results of an ATL Project
76 | [Dd]ebugPS/
77 | [Rr]eleasePS/
78 | dlldata.c
79 |
80 | # Benchmark Results
81 | BenchmarkDotNet.Artifacts/
82 |
83 | # .NET Core
84 | project.lock.json
85 | project.fragment.lock.json
86 | artifacts/
87 |
88 | # StyleCop
89 | StyleCopReport.xml
90 |
91 | # Files built by Visual Studio
92 | *_i.c
93 | *_p.c
94 | *_h.h
95 | *.ilk
96 | *.obj
97 | *.iobj
98 | *.pch
99 | *.pdb
100 | *.ipdb
101 | *.pgc
102 | *.pgd
103 | *.rsp
104 | *.sbr
105 | *.tlb
106 | *.tli
107 | *.tlh
108 | *.tmp
109 | *.tmp_proj
110 | *_wpftmp.csproj
111 | *.log
112 | *.vspscc
113 | *.vssscc
114 | .builds
115 | *.svclog
116 | *.scc
117 |
118 | # Chutzpah Test files
119 | _Chutzpah*
120 |
121 | # Visual C++ cache files
122 | ipch/
123 | *.aps
124 | *.ncb
125 | *.opendb
126 | *.opensdf
127 | *.sdf
128 | *.cachefile
129 | *.VC.db
130 | *.VC.VC.opendb
131 |
132 | # Visual Studio profiler
133 | *.psess
134 | *.vsp
135 | *.vspx
136 | *.sap
137 |
138 | # Visual Studio Trace Files
139 | *.e2e
140 |
141 | # TFS 2012 Local Workspace
142 | $tf/
143 |
144 | # Guidance Automation Toolkit
145 | *.gpState
146 |
147 | # ReSharper is a .NET coding add-in
148 | _ReSharper*/
149 | *.[Rr]e[Ss]harper
150 | *.DotSettings.user
151 |
152 | # JustCode is a .NET coding add-in
153 | .JustCode
154 |
155 | # TeamCity is a build add-in
156 | _TeamCity*
157 |
158 | # DotCover is a Code Coverage Tool
159 | *.dotCover
160 |
161 | # AxoCover is a Code Coverage Tool
162 | .axoCover/*
163 | !.axoCover/settings.json
164 |
165 | # Visual Studio code coverage results
166 | *.coverage
167 | *.coveragexml
168 |
169 | # NCrunch
170 | _NCrunch_*
171 | .*crunch*.local.xml
172 | nCrunchTemp_*
173 |
174 | # MightyMoose
175 | *.mm.*
176 | AutoTest.Net/
177 |
178 | # Web workbench (sass)
179 | .sass-cache/
180 |
181 | # Installshield output folder
182 | [Ee]xpress/
183 |
184 | # DocProject is a documentation generator add-in
185 | DocProject/buildhelp/
186 | DocProject/Help/*.HxT
187 | DocProject/Help/*.HxC
188 | DocProject/Help/*.hhc
189 | DocProject/Help/*.hhk
190 | DocProject/Help/*.hhp
191 | DocProject/Help/Html2
192 | DocProject/Help/html
193 |
194 | # Click-Once directory
195 | publish/
196 |
197 | # Publish Web Output
198 | *.[Pp]ublish.xml
199 | *.azurePubxml
200 | # Note: Comment the next line if you want to checkin your web deploy settings,
201 | # but database connection strings (with potential passwords) will be unencrypted
202 | *.pubxml
203 | *.publishproj
204 |
205 | # Microsoft Azure Web App publish settings. Comment the next line if you want to
206 | # checkin your Azure Web App publish settings, but sensitive information contained
207 | # in these scripts will be unencrypted
208 | PublishScripts/
209 |
210 | # NuGet Packages
211 | *.nupkg
212 | # NuGet Symbol Packages
213 | *.snupkg
214 | # The packages folder can be ignored because of Package Restore
215 | **/[Pp]ackages/*
216 | # except build/, which is used as an MSBuild target.
217 | !**/[Pp]ackages/build/
218 | # Uncomment if necessary however generally it will be regenerated when needed
219 | #!**/[Pp]ackages/repositories.config
220 | # NuGet v3's project.json files produces more ignorable files
221 | *.nuget.props
222 | *.nuget.targets
223 |
224 | # Microsoft Azure Build Output
225 | csx/
226 | *.build.csdef
227 |
228 | # Microsoft Azure Emulator
229 | ecf/
230 | rcf/
231 |
232 | # Windows Store app package directories and files
233 | AppPackages/
234 | BundleArtifacts/
235 | Package.StoreAssociation.xml
236 | _pkginfo.txt
237 | *.appx
238 | *.appxbundle
239 | *.appxupload
240 |
241 | # Visual Studio cache files
242 | # files ending in .cache can be ignored
243 | *.[Cc]ache
244 | # but keep track of directories ending in .cache
245 | !?*.[Cc]ache/
246 |
247 | # Others
248 | ClientBin/
249 | ~$*
250 | *~
251 | *.dbmdl
252 | *.dbproj.schemaview
253 | *.jfm
254 | *.pfx
255 | *.publishsettings
256 | orleans.codegen.cs
257 |
258 | # Including strong name files can present a security risk
259 | # (https://github.com/github/gitignore/pull/2483#issue-259490424)
260 | #*.snk
261 |
262 | # Since there are multiple workflows, uncomment next line to ignore bower_components
263 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622)
264 | #bower_components/
265 |
266 | # RIA/Silverlight projects
267 | Generated_Code/
268 |
269 | # Backup & report files from converting an old project file
270 | # to a newer Visual Studio version. Backup files are not needed,
271 | # because we have git ;-)
272 | _UpgradeReport_Files/
273 | Backup*/
274 | UpgradeLog*.XML
275 | UpgradeLog*.htm
276 | ServiceFabricBackup/
277 | *.rptproj.bak
278 |
279 | # SQL Server files
280 | *.mdf
281 | *.ldf
282 | *.ndf
283 |
284 | # Business Intelligence projects
285 | *.rdl.data
286 | *.bim.layout
287 | *.bim_*.settings
288 | *.rptproj.rsuser
289 | *- [Bb]ackup.rdl
290 | *- [Bb]ackup ([0-9]).rdl
291 | *- [Bb]ackup ([0-9][0-9]).rdl
292 |
293 | # Microsoft Fakes
294 | FakesAssemblies/
295 |
296 | # GhostDoc plugin setting file
297 | *.GhostDoc.xml
298 |
299 | # Node.js Tools for Visual Studio
300 | .ntvs_analysis.dat
301 | node_modules/
302 |
303 | # Visual Studio 6 build log
304 | *.plg
305 |
306 | # Visual Studio 6 workspace options file
307 | *.opt
308 |
309 | # Visual Studio 6 auto-generated workspace file (contains which files were open etc.)
310 | *.vbw
311 |
312 | # Visual Studio LightSwitch build output
313 | **/*.HTMLClient/GeneratedArtifacts
314 | **/*.DesktopClient/GeneratedArtifacts
315 | **/*.DesktopClient/ModelManifest.xml
316 | **/*.Server/GeneratedArtifacts
317 | **/*.Server/ModelManifest.xml
318 | _Pvt_Extensions
319 |
320 | # Paket dependency manager
321 | .paket/paket.exe
322 | paket-files/
323 |
324 | # FAKE - F# Make
325 | .fake/
326 |
327 | # CodeRush personal settings
328 | .cr/personal
329 |
330 | # Python Tools for Visual Studio (PTVS)
331 | __pycache__/
332 | *.pyc
333 |
334 | # Cake - Uncomment if you are using it
335 | # tools/**
336 | # !tools/packages.config
337 |
338 | # Tabs Studio
339 | *.tss
340 |
341 | # Telerik's JustMock configuration file
342 | *.jmconfig
343 |
344 | # BizTalk build output
345 | *.btp.cs
346 | *.btm.cs
347 | *.odx.cs
348 | *.xsd.cs
349 |
350 | # OpenCover UI analysis results
351 | OpenCover/
352 |
353 | # Azure Stream Analytics local run output
354 | ASALocalRun/
355 |
356 | # MSBuild Binary and Structured Log
357 | *.binlog
358 |
359 | # NVidia Nsight GPU debugger configuration file
360 | *.nvuser
361 |
362 | # MFractors (Xamarin productivity tool) working folder
363 | .mfractor/
364 |
365 | # Local History for Visual Studio
366 | .localhistory/
367 |
368 | # BeatPulse healthcheck temp database
369 | healthchecksdb
370 |
371 | # Backup folder for Package Reference Convert tool in Visual Studio 2017
372 | MigrationBackup/
373 |
374 | # End of https://www.gitignore.io/api/monodevelop,visualstudio,visualstudiocode
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | # lock distribution
2 | dist: trusty
3 |
4 | language: csharp
5 | solution: HttpSimulator.sln
6 | env:
7 | global:
8 | - EnableNuGetPackageRestore=true
9 | install:
10 | - nuget restore HttpSimulator.sln
11 | - nuget install NUnit.Runners -Version 2.6.4 -OutputDirectory testrunner
12 | script:
13 | - xbuild /p:Configuration=Release HttpSimulator.sln
14 | - mono ./testrunner/NUnit.Runners.2.6.4/tools/nunit-console.exe ./HttpSimulator.Tests/bin/Release/HttpSimulator.Tests.dll
15 | mono:
16 | - latest
17 | - 3.12.0
18 | - 3.10.0
19 |
20 |
--------------------------------------------------------------------------------
/HttpSimulator.Tests/HttpSimulator.Tests.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Debug
5 | AnyCPU
6 | 8.0.50727
7 | 2.0
8 | {7F177AF2-8C76-4E3C-93AA-C0EA5D23733F}
9 | Library
10 | HttpSimulatorTests
11 | HttpSimulator.Tests
12 | v4.0
13 |
14 |
15 |
16 |
17 | 2.0
18 |
19 | false
20 | publish\
21 | true
22 | Disk
23 | false
24 | Foreground
25 | 7
26 | Days
27 | false
28 | false
29 | true
30 | 0
31 | 1.0.0.%2a
32 | false
33 | true
34 |
35 |
36 | true
37 | full
38 | false
39 | bin\Debug
40 | DEBUG;
41 | prompt
42 | 4
43 | false
44 | AllRules.ruleset
45 |
46 |
47 | full
48 | true
49 | bin\Release
50 | prompt
51 | 4
52 | false
53 | AllRules.ruleset
54 |
55 |
56 |
57 | ..\packages\NUnit.2.6.4\lib\nunit.framework.dll
58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 | {CE9FFE92-9A64-483C-9F11-A27F83C0E14C}
69 | HttpSimulator
70 |
71 |
72 |
73 |
74 | False
75 | .NET Framework 3.5 SP1 Client Profile
76 | false
77 |
78 |
79 | False
80 | .NET Framework 3.5 SP1
81 | true
82 |
83 |
84 |
85 |
86 |
87 |
88 |
89 |
90 |
--------------------------------------------------------------------------------
/HttpSimulator.Tests/Test.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Specialized;
3 | using System.IO;
4 | using System.Security.Principal;
5 | using NUnit.Framework;
6 | using Http.TestLibrary;
7 | using System.Web;
8 |
9 | namespace HttpSimulatorTests
10 | {
11 | [TestFixture]
12 | public class Test
13 | {
14 | [Test]
15 | public void CurrentIsNotNull()
16 | {
17 | using (new HttpSimulator("/", @"c:\inetpub\").SimulateRequest())
18 | {
19 | Assert.IsNotNull(HttpContext.Current);
20 | }
21 | }
22 |
23 | [Test]
24 | public void RequestsAreLocalByDefault()
25 | {
26 | using (new HttpSimulator("/", @"c:\inetpub\").SimulateRequest())
27 | {
28 | Assert.IsTrue(HttpContext.Current.Request.IsLocal);
29 | }
30 | }
31 |
32 | [Test]
33 | public void CanSimulateRemoteRequests()
34 | {
35 | using (var simulator = new HttpSimulator())
36 | {
37 | simulator.SetIsLocalRequest(false)
38 | .SimulateRequest(new Uri("http://something.com/Test.aspx"), HttpVerb.GET);
39 |
40 | Assert.IsFalse(HttpContext.Current.Request.IsLocal);
41 | }
42 | }
43 |
44 | ///
45 | /// Determines whether this instance [can get set session].
46 | ///
47 | [Test]
48 | public void CanGetSetSession()
49 | {
50 | using (var simulator = new HttpSimulator("/", @"c:\inetpub\").SimulateRequest())
51 | {
52 | simulator.Context.Session["Test"] = "Success";
53 | Assert.AreEqual("Success", simulator.Context.Session["Test"]);
54 | }
55 | }
56 |
57 | [Test]
58 | public void CanSimulateFormGetWithQueryString()
59 | {
60 | using (var simulator = new HttpSimulator())
61 | {
62 | var form = new NameValueCollection();
63 | form.Add("Test1", "Value1");
64 | form.Add("Test2", "Value2");
65 | simulator.SimulateRequest(new Uri("http://localhost/Test.aspx?Test1=Value1&Test2=Value2"));
66 | Assert.AreEqual(form, simulator.Context.Request.QueryString);
67 | }
68 | }
69 |
70 | ///
71 | /// Determines whether this instance [can simulate form post].
72 | ///
73 | [Test]
74 | public void CanSimulateFormPost()
75 | {
76 | using (var simulator = new HttpSimulator())
77 | {
78 | var form = new NameValueCollection();
79 | form.Add("Test1", "Value1");
80 | form.Add("Test2", "Value2");
81 | simulator.SimulateRequest(new Uri("http://localhost/Test.aspx"), form);
82 |
83 | Assert.AreEqual("Value1", simulator.Context.Request.Form["Test1"]);
84 | Assert.AreEqual("Value2", simulator.Context.Request.Form["Test2"]);
85 | Assert.AreEqual(new Uri("http://localhost/Test.aspx"), simulator.Context.Request.Url);
86 | }
87 |
88 | using (var simulator = new HttpSimulator())
89 | {
90 | simulator.SetFormVariable("Test1", "Value1")
91 | .SetFormVariable("Test2", "Value2")
92 | .SimulateRequest(new Uri("http://localhost/Test.aspx"));
93 |
94 | Assert.AreEqual("Value1", simulator.Context.Request.Form["Test1"]);
95 | Assert.AreEqual("Value2", simulator.Context.Request.Form["Test2"]);
96 | Assert.AreEqual(new Uri("http://localhost/Test.aspx"), simulator.Context.Request.Url);
97 | }
98 | }
99 |
100 | [Test]
101 | public void CanSimulateSession()
102 | {
103 | using (var simulator = new HttpSimulator())
104 | {
105 | simulator.SimulateRequest(new Uri("http://localhost/Test.aspx"));
106 |
107 | Assert.IsNotNull(simulator.Context.Session.SessionID);
108 | simulator.Context.Session.Add("item", "value");
109 | Assert.AreEqual(1, simulator.Context.Session.Count);
110 | Assert.AreEqual("value", simulator.Context.Session["item"]);
111 | }
112 | }
113 |
114 | [Test]
115 | public void CanSimulateUserAgent() {
116 | using (var simulator = new HttpSimulator()) {
117 | var headers = new NameValueCollection();
118 | headers.Add("User-Agent", "Agent1");
119 | simulator.SimulateRequest(new Uri("http://localhost/Test.aspx"), HttpVerb.POST, headers);
120 | Assert.AreEqual("Agent1", simulator.Context.Request.UserAgent);
121 | }
122 | }
123 |
124 | [Test]
125 | [Platform(Exclude = "Mono")]
126 | public void CanSimulateCookie()
127 | {
128 | using (var simulator = new HttpSimulator())
129 | {
130 | var headers = new NameValueCollection();
131 | headers.Add("Cookie", "Cookie1=Value1");
132 | simulator.SimulateRequest(new Uri("http://localhost/Test.aspx"), HttpVerb.POST, headers);
133 | Assert.AreEqual("Value1", HttpContext.Current.Request.Cookies["Cookie1"].Value);
134 | }
135 | }
136 |
137 | [Test]
138 | [Platform(Exclude = "Mono")]
139 | public void CanSimulateBrowser()
140 | {
141 | using (var simulator = new HttpSimulator())
142 | {
143 | simulator.SetBrowser("browser", "IE");
144 | simulator.SimulateRequest(new Uri("http://localhost/Test.aspx"), HttpVerb.GET);
145 | Assert.NotNull(HttpContext.Current.Request.Browser);
146 | Assert.AreEqual("IE", HttpContext.Current.Request.Browser.Capabilities["browser"]);
147 | }
148 | }
149 |
150 | [Test]
151 | [Platform(Exclude = "Mono")]
152 | public void CanSimulateIdentity()
153 | {
154 | var id = new WindowsIdentity(WindowsIdentity.GetCurrent().Token,
155 | "Negotiate", WindowsAccountType.Normal, true);
156 |
157 | using (var simulator = new HttpSimulator())
158 | {
159 | simulator.SetIdentity(id);
160 | simulator.SimulateRequest(new Uri("http://localhost/Test.aspx"), HttpVerb.GET);
161 | Assert.NotNull(HttpContext.Current.Request.LogonUserIdentity);
162 | Assert.AreEqual("Negotiate", HttpContext.Current.Request.LogonUserIdentity.AuthenticationType);
163 | Assert.IsNotEmpty(HttpContext.Current.Request.LogonUserIdentity.Name);
164 | }
165 | }
166 |
167 | [Test]
168 | [Platform(Exclude = "Mono")]
169 | public void CanSimulateResponseHeaders()
170 | {
171 | using (var simulator = new HttpSimulator())
172 | {
173 | simulator.SimulateRequest(new Uri("http://localhost/Test.aspx"), HttpVerb.GET);
174 | HttpContext.Current.Response.AppendHeader("X-Content-Security-Policy", "frame-ancestors 'self'");
175 | HttpContext.Current.Response.Flush();
176 | Assert.IsNotEmpty(simulator.ResponseHeaders);
177 | Assert.That(simulator.ResponseHeaders, Contains.Substring("X-Content-Security-Policy: frame-ancestors 'self'"));
178 | }
179 | }
180 |
181 | [Test]
182 | [Platform(Exclude = "Mono")]
183 | public void CanGetSetCookies() {
184 | using (var simulator = new HttpSimulator())
185 | {
186 | simulator.SimulateRequest(new Uri("http://localhost/Test.aspx"), HttpVerb.POST);
187 | simulator.Context.Response.Cookies.Add(new HttpCookie("a", "b"));
188 | Assert.AreEqual("b", HttpContext.Current.Response.Cookies["a"].Value);
189 | }
190 | }
191 |
192 | [Test]
193 | [Platform(Exclude = "Mono")]
194 | public void CanSimulateMapPath()
195 | {
196 | using (var simulator = new HttpSimulator())
197 | {
198 | simulator.SimulateRequest(new Uri("http://localhost/Test.aspx"));
199 |
200 | Assert.AreEqual("c:\\InetPub\\wwwRoot\\test\\test.js", simulator.Context.Server.MapPath("~/test/test.js"));
201 | Assert.AreEqual("c:\\InetPub\\wwwRoot\\test.js", simulator.Context.Server.MapPath("test.js"));
202 | }
203 | }
204 |
205 | ///
206 | /// Determines whether this instance [can simulate form post].
207 | ///
208 | [Test]
209 | [Platform(Exclude = "Mono")]
210 | public void CanSimulateFormPostOnHttpContext()
211 | {
212 | using (var simulator = new HttpSimulator())
213 | {
214 | var form = new NameValueCollection();
215 | form.Add("Test1", "Value1");
216 | form.Add("Test2", "Value2");
217 | simulator.SimulateRequest(new Uri("http://localhost/Test.aspx"), form);
218 |
219 | Assert.AreEqual("Value1", HttpContext.Current.Request.Form["Test1"]);
220 | Assert.AreEqual("Value2", HttpContext.Current.Request.Form["Test2"]);
221 | Assert.AreEqual(new Uri("http://localhost/Test.aspx"), HttpContext.Current.Request.Url);
222 | }
223 |
224 | using (var simulator = new HttpSimulator())
225 | {
226 | simulator.SetFormVariable("Test1", "Value1")
227 | .SetFormVariable("Test2", "Value2")
228 | .SimulateRequest(new Uri("http://localhost/Test.aspx"));
229 |
230 | Assert.AreEqual("Value1", HttpContext.Current.Request.Form["Test1"]);
231 | Assert.AreEqual("Value2", HttpContext.Current.Request.Form["Test2"]);
232 | Assert.AreEqual(new Uri("http://localhost/Test.aspx"), HttpContext.Current.Request.Url);
233 | }
234 | }
235 |
236 | [Test]
237 | [Platform(Exclude = "Mono")]
238 | public void CanWriteDebugInfoToSpecifiedWriter()
239 | {
240 | using (var debugWriter = new StringWriter())
241 | {
242 | using (var simulator = new HttpSimulator())
243 | {
244 | simulator.DebugWriter = debugWriter;
245 |
246 | simulator.SimulateRequest();
247 |
248 | Assert.IsNotNullOrEmpty(debugWriter.ToString(), "Debug output");
249 | }
250 | }
251 | }
252 |
253 | [Test]
254 | [Platform(Exclude = "Mono")]
255 | public void CanDisableWritingDebugInfoToConsole()
256 | {
257 | using (var console = new ConsoleCapture())
258 | {
259 | using (var simulator = new HttpSimulator())
260 | {
261 | simulator.DebugWriter = TextWriter.Null;
262 | simulator.SimulateRequest();
263 | Assert.IsEmpty(console.Out.ToString(), "Console.Out output");
264 | }
265 | }
266 | }
267 |
268 | ///
269 | /// Captures ouput that would go to the console.
270 | ///
271 | private class ConsoleCapture : IDisposable
272 | {
273 | private readonly TextWriter savedStdOut;
274 | private readonly TextWriter savedStdErr;
275 |
276 | public TextWriter Out { get; private set; } = new StringWriter();
277 |
278 | public TextWriter Error { get; private set; } = new StringWriter();
279 |
280 |
281 | public ConsoleCapture()
282 | {
283 | savedStdOut = Console.Out;
284 | savedStdErr = Console.Error;
285 |
286 | Console.SetOut(Out);
287 | Console.SetError(Error);
288 | }
289 |
290 | public void Dispose()
291 | {
292 | Console.SetOut(savedStdOut);
293 | Console.SetError(savedStdErr);
294 |
295 | Out?.Dispose();
296 | Out = null;
297 | Error?.Dispose();
298 | Error = null;
299 | }
300 | }
301 | }
302 | }
303 |
304 |
--------------------------------------------------------------------------------
/HttpSimulator.Tests/packages.config:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/HttpSimulator.sln:
--------------------------------------------------------------------------------
1 |
2 | Microsoft Visual Studio Solution File, Format Version 12.00
3 | # Visual Studio 15
4 | VisualStudioVersion = 15.0.26403.7
5 | MinimumVisualStudioVersion = 10.0.40219.1
6 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "HttpSimulator", "HttpSimulator\HttpSimulator.csproj", "{CE9FFE92-9A64-483C-9F11-A27F83C0E14C}"
7 | EndProject
8 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "HttpSimulator.Tests", "HttpSimulator.Tests\HttpSimulator.Tests.csproj", "{7F177AF2-8C76-4E3C-93AA-C0EA5D23733F}"
9 | EndProject
10 | Global
11 | GlobalSection(SolutionConfigurationPlatforms) = preSolution
12 | Debug|Any CPU = Debug|Any CPU
13 | Release|Any CPU = Release|Any CPU
14 | EndGlobalSection
15 | GlobalSection(ProjectConfigurationPlatforms) = postSolution
16 | {CE9FFE92-9A64-483C-9F11-A27F83C0E14C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
17 | {CE9FFE92-9A64-483C-9F11-A27F83C0E14C}.Debug|Any CPU.Build.0 = Debug|Any CPU
18 | {CE9FFE92-9A64-483C-9F11-A27F83C0E14C}.Release|Any CPU.ActiveCfg = Release|Any CPU
19 | {CE9FFE92-9A64-483C-9F11-A27F83C0E14C}.Release|Any CPU.Build.0 = Release|Any CPU
20 | {7F177AF2-8C76-4E3C-93AA-C0EA5D23733F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
21 | {7F177AF2-8C76-4E3C-93AA-C0EA5D23733F}.Debug|Any CPU.Build.0 = Debug|Any CPU
22 | {7F177AF2-8C76-4E3C-93AA-C0EA5D23733F}.Release|Any CPU.ActiveCfg = Release|Any CPU
23 | {7F177AF2-8C76-4E3C-93AA-C0EA5D23733F}.Release|Any CPU.Build.0 = Release|Any CPU
24 | EndGlobalSection
25 | GlobalSection(SolutionProperties) = preSolution
26 | HideSolutionNode = FALSE
27 | EndGlobalSection
28 | EndGlobal
29 |
--------------------------------------------------------------------------------
/HttpSimulator/BaseWrapped/HttpContext.cs:
--------------------------------------------------------------------------------
1 | using System.IO;
2 | using System.Web;
3 |
4 | namespace Http.TestLibrary.BaseWrapped
5 | {
6 | internal class HttpContext : HttpContextBase
7 | {
8 | private readonly HttpRequestBase _workerRequest;
9 | private readonly HttpSessionStateBase _fakeHttpSessionState;
10 | private readonly HttpServerUtility _fakeHttpServerUtility;
11 | private readonly HttpResponseBase _fakeHttpResponse;
12 |
13 | public HttpContext(HttpRequestBase workerRequest, HttpSessionStateBase fakeHttpSessionState, HttpServerUtility fakeHttpServerUtility, HttpResponse response)
14 | {
15 | _workerRequest = workerRequest;
16 | _fakeHttpSessionState = fakeHttpSessionState;
17 | _fakeHttpServerUtility = fakeHttpServerUtility;
18 | _fakeHttpResponse = new HttpResponseWrapper(response);
19 | }
20 |
21 | public override HttpSessionStateBase Session
22 | {
23 | get { return _fakeHttpSessionState; }
24 | }
25 |
26 | public override HttpRequestBase Request
27 | {
28 | get { return _workerRequest; }
29 | }
30 |
31 | public override HttpServerUtilityBase Server
32 | {
33 | get { return _fakeHttpServerUtility; }
34 | }
35 |
36 | public override HttpResponseBase Response {
37 | get {
38 | return _fakeHttpResponse;
39 | }
40 | }
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/HttpSimulator/BaseWrapped/HttpServerUtility.cs:
--------------------------------------------------------------------------------
1 | using System.Web;
2 |
3 | namespace Http.TestLibrary.BaseWrapped
4 | {
5 | internal class HttpServerUtility : HttpServerUtilityBase
6 | {
7 | private readonly HttpSimulator.ConfigMapPath _configMap;
8 |
9 | public HttpServerUtility(HttpSimulator.ConfigMapPath configMap)
10 | {
11 | _configMap = configMap;
12 | }
13 |
14 | public override string MapPath(string path)
15 | {
16 | return _configMap.MapPath(string.Empty, path);
17 | }
18 | }
19 | }
--------------------------------------------------------------------------------
/HttpSimulator/BaseWrapped/HttpSessionState.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Specialized;
3 | using System.Web;
4 | using System.Web.SessionState;
5 |
6 | namespace Http.TestLibrary.BaseWrapped
7 | {
8 | internal class HttpSessionState : HttpSessionStateBase
9 | {
10 | private HttpSimulator.FakeHttpSessionState session;
11 |
12 | public HttpSessionState(HttpSimulator.FakeHttpSessionState session)
13 | {
14 | this.session = session;
15 | }
16 |
17 | ///
18 | ///Ends the current session.
19 | ///
20 | ///
21 | public override void Abandon()
22 | {
23 | session.Abandon();
24 | }
25 |
26 | ///
27 | ///Adds a new item to the session-state collection.
28 | ///
29 | ///
30 | ///The name of the item to add to the session-state collection.
31 | ///The value of the item to add to the session-state collection.
32 | public override void Add(string name, object value)
33 | {
34 | session.Add(name, value);
35 | }
36 |
37 | ///
38 | ///Deletes an item from the session-state item collection.
39 | ///
40 | ///
41 | ///The name of the item to delete from the session-state item collection.
42 | public override void Remove(string name)
43 | {
44 | session.Remove(name);
45 | }
46 |
47 | ///
48 | ///Deletes an item at a specified index from the session-state item collection.
49 | ///
50 | ///
51 | ///The index of the item to remove from the session-state collection.
52 | public override void RemoveAt(int index)
53 | {
54 | session.RemoveAt(index);
55 | }
56 |
57 | ///
58 | ///Clears all values from the session-state item collection.
59 | ///
60 | ///
61 | public override void Clear()
62 | {
63 | session.Clear();
64 | }
65 |
66 | ///
67 | ///Clears all values from the session-state item collection.
68 | ///
69 | ///
70 | public override void RemoveAll()
71 | {
72 | session.RemoveAll();
73 | }
74 |
75 | ///
76 | ///Copies the collection of session-state item values to a one-dimensional array, starting at the specified index in the array.
77 | ///
78 | ///
79 | ///The that receives the session values.
80 | ///The index in array where copying starts.
81 | public override void CopyTo(Array array, int index)
82 | {
83 | throw new NotImplementedException();
84 | }
85 |
86 | ///
87 | ///Gets the unique session identifier for the session.
88 | ///
89 | ///
90 | ///
91 | ///The session ID.
92 | ///
93 | ///
94 | public override string SessionID
95 | {
96 | get { return session.SessionID; }
97 | }
98 |
99 | ///
100 | ///Gets and sets the time-out period (in minutes) allowed between requests before the session-state provider terminates the session.
101 | ///
102 | ///
103 | ///
104 | ///The time-out period, in minutes.
105 | ///
106 | ///
107 | public override int Timeout
108 | {
109 | get { return session.Timeout; }
110 | set { session.Timeout = value; }
111 | }
112 |
113 | ///
114 | ///Gets a value indicating whether the session was created with the current request.
115 | ///
116 | ///
117 | ///
118 | ///true if the session was created with the current request; otherwise, false.
119 | ///
120 | ///
121 | public override bool IsNewSession
122 | {
123 | get { return session.IsNewSession; }
124 | }
125 |
126 | ///
127 | ///Gets the current session-state mode.
128 | ///
129 | ///
130 | ///
131 | ///One of the values.
132 | ///
133 | ///
134 | public override SessionStateMode Mode
135 | {
136 | get { return session.Mode; }
137 | }
138 |
139 | ///
140 | ///Gets a value indicating whether the session ID is embedded in the URL or stored in an HTTP cookie.
141 | ///
142 | ///
143 | ///
144 | ///true if the session is embedded in the URL; otherwise, false.
145 | ///
146 | ///
147 | public override bool IsCookieless
148 | {
149 | get { return session.IsCookieless; }
150 | }
151 |
152 | ///
153 | ///Gets a value that indicates whether the application is configured for cookieless sessions.
154 | ///
155 | ///
156 | ///
157 | ///One of the values that indicate whether the application is configured for cookieless sessions. The default is .
158 | ///
159 | ///
160 | public override HttpCookieMode CookieMode
161 | {
162 | get { return session.CookieMode; }
163 | }
164 |
165 | ///
166 | ///Gets or sets the locale identifier (LCID) of the current session.
167 | ///
168 | ///
169 | ///
170 | ///A instance that specifies the culture of the current session.
171 | ///
172 | ///
173 | public override int LCID
174 | {
175 | get { return session.LCID; }
176 | set { session.LCID = value; }
177 | }
178 |
179 | ///
180 | ///Gets or sets the code-page identifier for the current session.
181 | ///
182 | ///
183 | ///
184 | ///The code-page identifier for the current session.
185 | ///
186 | ///
187 | public override int CodePage
188 | {
189 | get { return session.CodePage; }
190 | set { session.CodePage = value; }
191 | }
192 |
193 | ///
194 | ///Gets a collection of objects declared by <object Runat="Server" Scope="Session"/> tags within the ASP.NET application file Global.asax.
195 | ///
196 | ///
197 | ///
198 | ///An containing objects declared in the Global.asax file.
199 | ///
200 | ///
201 | public override HttpStaticObjectsCollectionBase StaticObjects
202 | {
203 | get
204 | {
205 | throw new NotImplementedException();
206 | //return session.StaticObjects;
207 | }
208 | }
209 |
210 | ///
211 | ///Gets or sets a session-state item value by name.
212 | ///
213 | ///
214 | ///
215 | ///The session-state item value specified in the name parameter.
216 | ///
217 | ///
218 | ///The key name of the session-state item value.
219 | public override object this [string name]
220 | {
221 | get { return session[name]; }
222 | set { session[name] = value; }
223 | }
224 |
225 | ///
226 | ///Gets or sets a session-state item value by numerical index.
227 | ///
228 | ///
229 | ///
230 | ///The session-state item value specified in the index parameter.
231 | ///
232 | ///
233 | ///The numerical index of the session-state item value.
234 | public override object this [int index]
235 | {
236 | get { return session[index]; }
237 | set { session[index] = value; }
238 | }
239 |
240 | ///
241 | ///Gets an object that can be used to synchronize access to the collection of session-state values.
242 | ///
243 | ///
244 | ///
245 | ///An object that can be used to synchronize access to the collection.
246 | ///
247 | ///
248 | public override object SyncRoot
249 | {
250 | get { return session.SyncRoot; }
251 | }
252 |
253 | ///
254 | ///Gets a value indicating whether access to the collection of session-state values is synchronized (thread safe).
255 | ///
256 | ///
257 | ///true if access to the collection is synchronized (thread safe); otherwise, false.
258 | ///
259 | ///
260 | public override bool IsSynchronized
261 | {
262 | get { return session.IsSynchronized; }
263 | }
264 |
265 | public override int Count
266 | {
267 | get { return session.Count; }
268 | }
269 |
270 | public override bool IsReadOnly
271 | {
272 | get { return session.GetIsReadOnly(); }
273 | }
274 |
275 | public override NameObjectCollectionBase.KeysCollection Keys
276 | {
277 | get { return session.Keys; }
278 | }
279 |
280 | }
281 | }
--------------------------------------------------------------------------------
/HttpSimulator/BaseWrapped/SimulatedHttpRequest.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Specialized;
3 | using System.Web;
4 |
5 | namespace Http.TestLibrary.BaseWrapped
6 | {
7 | internal class SimulatedHttpRequest:HttpRequestBase
8 | {
9 | private readonly TestLibrary.SimulatedHttpRequest _request;
10 |
11 | internal void SetReferer(Uri referer)
12 | {
13 | _request.SetReferer(referer);
14 | }
15 |
16 | ///
17 | /// Returns the specified member of the request header.
18 | ///
19 | ///
20 | /// The HTTP verb returned in the request
21 | /// header.
22 | ///
23 | public override string HttpMethod
24 | {
25 | get { return _request.GetHttpVerbName(); }
26 | }
27 |
28 |
29 | ///
30 | /// Gets the name of the server.
31 | ///
32 | ///
33 | public string GetServerName()
34 | {
35 | return _request.GetServerName();
36 | }
37 |
38 | public int GetLocalPort()
39 | {
40 | return _request.GetLocalPort();
41 | }
42 |
43 | ///
44 | /// Gets the headers.
45 | ///
46 | /// The headers.
47 | public override NameValueCollection Headers
48 | {
49 | get
50 | {
51 | return _request.Headers;
52 | }
53 | }
54 |
55 |
56 | ///
57 | /// Gets the format exception.
58 | ///
59 | /// The format exception.
60 | public override NameValueCollection Form
61 | {
62 | get
63 | {
64 | return _request.Form;
65 | }
66 | }
67 |
68 | public override NameValueCollection QueryString
69 | {
70 | get
71 | {
72 | return HttpUtility.ParseQueryString(_request.GetQueryString());
73 | }
74 |
75 | }
76 |
77 | public SimulatedHttpRequest(TestLibrary.SimulatedHttpRequest request)
78 | {
79 | _request = request;
80 | }
81 |
82 | ///
83 | /// Returns the virtual path to the currently executing
84 | /// server application.
85 | ///
86 | ///
87 | /// The virtual path of the current application.
88 | ///
89 | public override string ApplicationPath
90 | {
91 | get { return _request.GetAppPath(); }
92 | }
93 |
94 | public override string PhysicalApplicationPath
95 | {
96 | get { return _request.GetAppPathTranslated(); }
97 | }
98 |
99 | public override Uri Url
100 | {
101 | get { return _request.Uri; }
102 | }
103 |
104 |
105 | public override string UserAgent {
106 | get {
107 | return _request.GetKnownRequestHeader(HttpWorkerRequest.HeaderUserAgent);
108 | }
109 | }
110 | }
111 | }
--------------------------------------------------------------------------------
/HttpSimulator/HttpSimulator.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Specialized;
3 | using System.IO;
4 | using System.Security.Principal;
5 | using System.Text;
6 | using System.Text.RegularExpressions;
7 | using System.Web;
8 | using System.Web.Configuration;
9 | using System.Web.Hosting;
10 | using System.Web.SessionState;
11 | using Http.TestLibrary.BaseWrapped;
12 | using HttpContext = Http.TestLibrary.BaseWrapped.HttpContext;
13 | using HttpSessionState = Http.TestLibrary.BaseWrapped.HttpSessionState;
14 |
15 | namespace Http.TestLibrary
16 | {
17 | public enum HttpVerb
18 | {
19 | GET,
20 | HEAD,
21 | POST,
22 | PUT,
23 | DELETE,
24 | }
25 |
26 | ///
27 | /// Useful class for simulating the HttpContext. This does not actually
28 | /// make an HttpRequest, it merely simulates the state that your code
29 | /// would be in "as if" handling a request. Thus the HttpContext.Current
30 | /// property is populated.
31 | ///
32 | public class HttpSimulator : IDisposable
33 | {
34 | private const string defaultPhysicalAppPath = @"c:\InetPub\wwwRoot\";
35 | private StringBuilder builder;
36 | private Uri _referer;
37 | private bool _isLocalRequest = true;
38 | private NameValueCollection _formVars = new NameValueCollection();
39 | private NameValueCollection _headers = new NameValueCollection();
40 | private NameValueCollection _browser = new NameValueCollection();
41 | private WindowsIdentity _identity;
42 | private TextWriter debugWriter = Console.Out;
43 |
44 | public HttpSimulator()
45 | : this("/", defaultPhysicalAppPath)
46 | {
47 | }
48 |
49 | public HttpSimulator(string applicationPath)
50 | : this(applicationPath, defaultPhysicalAppPath)
51 | {
52 | }
53 |
54 | public HttpSimulator(string applicationPath, string physicalApplicationPath)
55 | {
56 | this.ApplicationPath = applicationPath;
57 | this.PhysicalApplicationPath = physicalApplicationPath;
58 | }
59 |
60 | ///
61 | /// Sets up the HttpContext objects to simulate a GET request.
62 | ///
63 | ///
64 | /// Simulates a request to http://localhost/
65 | ///
66 | public HttpSimulator SimulateRequest()
67 | {
68 | return SimulateRequest(new Uri("http://localhost/"));
69 | }
70 |
71 | ///
72 | /// Sets up the HttpContext objects to simulate a GET request.
73 | ///
74 | ///
75 | public HttpSimulator SimulateRequest(Uri url)
76 | {
77 | return SimulateRequest(url, HttpVerb.GET);
78 | }
79 |
80 | ///
81 | /// Sets up the HttpContext objects to simulate a request.
82 | ///
83 | ///
84 | ///
85 | public HttpSimulator SimulateRequest(Uri url, HttpVerb httpVerb)
86 | {
87 | return SimulateRequest(url, httpVerb, null, null);
88 | }
89 |
90 | ///
91 | /// Sets up the HttpContext objects to simulate a POST request.
92 | ///
93 | ///
94 | ///
95 | public HttpSimulator SimulateRequest(Uri url, NameValueCollection formVariables)
96 | {
97 | return SimulateRequest(url, HttpVerb.POST, formVariables, null);
98 | }
99 |
100 | ///
101 | /// Sets up the HttpContext objects to simulate a POST request.
102 | ///
103 | ///
104 | ///
105 | ///
106 | public HttpSimulator SimulateRequest(Uri url, NameValueCollection formVariables, NameValueCollection headers)
107 | {
108 | return SimulateRequest(url, HttpVerb.POST, formVariables, headers);
109 | }
110 |
111 | ///
112 | /// Sets up the HttpContext objects to simulate a request.
113 | ///
114 | ///
115 | ///
116 | ///
117 | public HttpSimulator SimulateRequest(Uri url, HttpVerb httpVerb, NameValueCollection headers)
118 | {
119 | return SimulateRequest(url, httpVerb, null, headers);
120 | }
121 |
122 | ///
123 | /// Sets up the HttpContext objects to simulate a request.
124 | ///
125 | ///
126 | ///
127 | ///
128 | ///
129 | protected virtual HttpSimulator SimulateRequest(Uri url, HttpVerb httpVerb, NameValueCollection formVariables, NameValueCollection headers)
130 | {
131 | System.Web.HttpContext.Current = null;
132 |
133 | ParseRequestUrl(url);
134 |
135 | if (this.responseWriter == null)
136 | {
137 | this.builder = new StringBuilder();
138 | this.responseWriter = new StringWriter(builder);
139 | }
140 |
141 | _responseHeadersBuilder = new StringBuilder();
142 |
143 | SetHttpRuntimeInternals();
144 |
145 | string query = ExtractQueryStringPart(url);
146 |
147 | if (formVariables != null)
148 | _formVars.Add(formVariables);
149 |
150 | if (_formVars.Count > 0)
151 | httpVerb = HttpVerb.POST; //Need to enforce this.
152 |
153 | if (headers != null)
154 | _headers.Add(headers);
155 |
156 | this.workerRequest = new SimulatedHttpRequest(ApplicationPath, PhysicalApplicationPath, PhysicalPath, Page, query, this.responseWriter, host, port, httpVerb.ToString(), url, _responseHeadersBuilder);
157 |
158 | this.workerRequest.Form.Add(_formVars);
159 | this.workerRequest.Headers.Add(_headers);
160 | foreach(var key in _browser.AllKeys)
161 | this.workerRequest.Browser.Capabilities.Add(key, _browser.Get(key));
162 |
163 | if (_referer != null)
164 | this.workerRequest.SetReferer(_referer);
165 |
166 | if (_identity != null)
167 | this.workerRequest.SetIdentity(_identity);
168 |
169 | this.workerRequest.SetIsLocalRequest(_isLocalRequest);
170 |
171 | InitializeSession();
172 |
173 | InitializeApplication();
174 |
175 | WriteDebugInfo();
176 |
177 | return this;
178 | }
179 |
180 | ///
181 | /// Sets the to use for writing out debugging information.
182 | ///
183 | /// Set this to TextWriter.Null to disable debug output.
184 | ///
185 | ///
186 | /// By default, debug information is written out to the console.
187 | ///
188 | public TextWriter DebugWriter { set { debugWriter = value; } }
189 |
190 | private void WriteDebugInfo()
191 | {
192 | if (debugWriter == null)
193 | {
194 | return;
195 | }
196 | debugWriter.WriteLine("host: " + host);
197 | debugWriter.WriteLine("virtualDir: " + applicationPath);
198 | debugWriter.WriteLine("page: " + localPath);
199 | debugWriter.WriteLine("pathPartAfterApplicationPart: " + _page);
200 | debugWriter.WriteLine("appPhysicalDir: " + physicalApplicationPath);
201 | debugWriter.WriteLine("Request.Url.LocalPath: " + System.Web.HttpContext.Current.Request.Url.LocalPath);
202 | debugWriter.WriteLine("Request.Url.Host: " + System.Web.HttpContext.Current.Request.Url.Host);
203 | debugWriter.WriteLine("Request.FilePath: " + System.Web.HttpContext.Current.Request.FilePath);
204 | debugWriter.WriteLine("Request.Path: " + System.Web.HttpContext.Current.Request.Path);
205 | debugWriter.WriteLine("Request.RawUrl: " + System.Web.HttpContext.Current.Request.RawUrl);
206 | debugWriter.WriteLine("Request.Url: " + System.Web.HttpContext.Current.Request.Url);
207 | debugWriter.WriteLine("Request.Url.Port: " + System.Web.HttpContext.Current.Request.Url.Port);
208 | debugWriter.WriteLine("Request.ApplicationPath: " + System.Web.HttpContext.Current.Request.ApplicationPath);
209 | debugWriter.WriteLine("Request.PhysicalPath: " + System.Web.HttpContext.Current.Request.PhysicalPath);
210 | debugWriter.WriteLine("HttpRuntime.AppDomainAppPath: " + HttpRuntime.AppDomainAppPath);
211 | debugWriter.WriteLine("HttpRuntime.AppDomainAppVirtualPath: " + HttpRuntime.AppDomainAppVirtualPath);
212 | debugWriter.WriteLine("HostingEnvironment.ApplicationPhysicalPath: " + HostingEnvironment.ApplicationPhysicalPath);
213 | debugWriter.WriteLine("HostingEnvironment.ApplicationVirtualPath: " + HostingEnvironment.ApplicationVirtualPath);
214 | }
215 |
216 | private static void InitializeApplication()
217 | {
218 | Type appFactoryType = Type.GetType("System.Web.HttpApplicationFactory, System.Web, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a");
219 | object appFactory = ReflectionHelper.GetStaticFieldValue